diff --git a/.gitignore b/.gitignore index 32512494..660f31f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,11 @@ *.iml +*.aab .gradle /local.properties +/gradle.properties /.idea/ .DS_Store /build /captures -release.keystore -signing.properties -/library/build +keystore.jks +keystore.properties diff --git a/CHANGELOG.md b/CHANGELOG.md index d7902ed2..6ace45ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,33 @@ Changelog ========== +Version 6.1.0 *(2018-11-30)* +---------------------------- + + * Allow setting the app as the default for handling calls + * Allow blocking numbers on Android 7+ + * Improved contact recognition at Recents + * Fixed some handling related to creating new contact with a number, or adding a number to an existing contact + * Add handling for secret codes like *#*#4636#*#* + * Allow copying all contact fields from the view screen by long pressing them + * Added an option for hiding the dialpad button on the main screen + * Fixed some issues related to importing contacts with birthdays + +Version 6.0.0 *(2018-11-06)* +---------------------------- + + * Initial Pro version + +Version 5.1.2 *(2018-11-28)* +---------------------------- + + * Had to remove the Recents tab due to the latest Googles' permissions policy. The Pro app will be updated with a Recents tab and a mandatory Dialer soon. + * This version of the app is no longer maintained, please upgrade to the Pro version. You can find the Upgrade button at the top of the app Settings. + Version 5.1.1 *(2018-11-05)* ---------------------------- - * This version of the app is no longer maintained. Please upgrade to the Pro version. It is free till Nov 13 2018. You can find the Upgrade button at the top of the app Settings. + * Added some stability and translation improvements Version 5.1.0 *(2018-10-28)* ---------------------------- diff --git a/README.md b/README.md index b7f75f1a..628bd7d1 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ Contains no ads or unnecessary permissions. It is fully opensource, provides cus This app is just one piece of a bigger series of apps. You can find the rest of them at http://www.simplemobiletools.com -Get it on Google Play -Get it on F-Droid +Get it on Google Play +Get it on F-Droid
App image diff --git a/app/build.gradle b/app/build.gradle index 93de4f18..5d27b633 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,11 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' + +def keystorePropertiesFile = rootProject.file("keystore.properties") +def keystoreProperties = new Properties() +keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) android { compileSdkVersion 28 @@ -10,13 +15,18 @@ android { applicationId "com.simplemobiletools.contacts.pro" minSdkVersion 21 targetSdkVersion 28 - versionCode 35 - versionName "5.1.1" + versionCode 37 + versionName "6.1.0" setProperty("archivesBaseName", "contacts") } signingConfigs { - release + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile file(keystoreProperties['storeFile']) + storePassword keystoreProperties['storePassword'] + } } buildTypes { @@ -41,28 +51,12 @@ android { } dependencies { - implementation 'com.simplemobiletools:commons:5.3.11' - implementation 'joda-time:joda-time:2.9.9' - implementation 'com.facebook.stetho:stetho:1.5.0' + implementation 'com.simplemobiletools:commons:5.5.1' + implementation 'joda-time:joda-time:2.10.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2' implementation 'com.googlecode.ez-vcard:ez-vcard:0.10.4' -} -Properties props = new Properties() -def propFile = new File('signing.properties') -if (propFile.canRead()) { - props.load(new FileInputStream(propFile)) - - if (props != null && props.containsKey('STORE_FILE') && props.containsKey('KEY_ALIAS') && props.containsKey('PASSWORD')) { - android.signingConfigs.release.storeFile = file(props['STORE_FILE']) - android.signingConfigs.release.storePassword = props['PASSWORD'] - android.signingConfigs.release.keyAlias = props['KEY_ALIAS'] - android.signingConfigs.release.keyPassword = props['PASSWORD'] - } else { - println 'signing.properties found but some entries are missing' - android.buildTypes.release.signingConfig = null - } -} else { - println 'signing.properties not found' - android.buildTypes.release.signingConfig = null + kapt "androidx.room:room-compiler:2.0.0" + implementation "androidx.room:room-runtime:2.0.0" + annotationProcessor "androidx.room:room-compiler:2.0.0" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 81d50163..c965a109 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ android:installLocation="auto"> + @@ -13,6 +14,11 @@ + + + + + + + + + + + + + + + + + android:parentActivityName=".activities.MainActivity"> + + + + + + + + + + + + + + + + + + + + + + + + () + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_dialpad) @@ -70,6 +79,14 @@ class DialpadActivity : SimpleActivity() { return true } + private fun checkDialIntent() { + if (intent.action == Intent.ACTION_DIAL && intent.data != null && intent.dataString?.contains("tel:") == true) { + val number = Uri.decode(intent.dataString).substringAfter("tel:") + dialpad_input.setText(number) + dialpad_input.setSelection(number.length) + } + } + private fun addNumberToContact() { Intent().apply { action = Intent.ACTION_INSERT_OR_EDIT @@ -121,12 +138,27 @@ class DialpadActivity : SimpleActivity() { private fun gotContacts(newContacts: ArrayList) { contacts = newContacts - Contact.sorting = config.sorting - Contact.startWithSurname = config.startNameWithSurname - contacts.sort() + checkDialIntent() } + @TargetApi(Build.VERSION_CODES.O) private fun dialpadValueChanged(text: String) { + val len = text.length + if (len > 8 && text.startsWith("*#*#") && text.endsWith("#*#*")) { + val secretCode = text.substring(4, text.length - 4) + if (isOreoPlus()) { + if (isDefaultDialer()) { + getSystemService(TelephonyManager::class.java).sendDialerSpecialCode(secretCode) + } else { + launchSetDefaultDialerIntent() + } + } else { + val intent = Intent(SECRET_CODE_ACTION, Uri.parse("android_secret_code://$secretCode")) + sendBroadcast(intent) + } + return + } + (dialpad_list.adapter as? ContactsAdapter)?.finishActMode() val filtered = contacts.filter { it.doesContainPhoneNumber(text) } as ArrayList @@ -144,6 +176,13 @@ class DialpadActivity : SimpleActivity() { } } + override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) { + super.onActivityResult(requestCode, resultCode, resultData) + if (requestCode == REQUEST_CODE_SET_DEFAULT_DIALER && isDefaultDialer()) { + dialpadValueChanged(dialpad_input.value) + } + } + private fun initCall() { val number = dialpad_input.value if (number.isNotEmpty()) { diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/EditContactActivity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/EditContactActivity.kt index 51fc4760..f882d3c3 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/EditContactActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/EditContactActivity.kt @@ -91,7 +91,7 @@ class EditContactActivity : ContactActivity() { if (wasActivityInitialized) { menu.findItem(R.id.delete).isVisible = contact?.id != 0 menu.findItem(R.id.share).isVisible = contact?.id != 0 - menu.findItem(R.id.open_with).isVisible = contact?.id != 0 && contact?.source != SMT_PRIVATE + menu.findItem(R.id.open_with).isVisible = contact?.id != 0 && contact?.isPrivate() == false } return true } @@ -128,7 +128,11 @@ class EditContactActivity : ContactActivity() { val data = intent.data if (data != null) { val rawId = if (data.path.contains("lookup")) { - getLookupUriRawId(data) + if (data.pathSegments.last().startsWith("local_")) { + data.path.substringAfter("local_").toInt() + } else { + getLookupUriRawId(data) + } } else { getContactUriRawId(data) } @@ -140,36 +144,46 @@ class EditContactActivity : ContactActivity() { } if (contactId != 0) { - contact = ContactsHelper(this).getContactWithId(contactId, intent.getBooleanExtra(IS_PRIVATE, false)) - if (contact == null) { - toast(R.string.unknown_error_occurred) - finish() - return - } + Thread { + contact = ContactsHelper(this).getContactWithId(contactId, intent.getBooleanExtra(IS_PRIVATE, false)) + if (contact == null) { + toast(R.string.unknown_error_occurred) + finish() + } else { + runOnUiThread { + gotContact() + } + } + }.start() + } else { + gotContact() } + } + private fun gotContact() { + contact_scrollview.beVisible() if (contact == null) { setupNewContact() } else { setupEditContact() } - if ((contact!!.id == 0 && intent.extras != null && intent.extras.containsKey(KEY_PHONE) && action == Intent.ACTION_INSERT) || action == ADD_NEW_CONTACT_NUMBER) { - val phone = intent.extras.get(KEY_PHONE) - if (phone != null) { - val phoneNumber = phone.toString() - contact!!.phoneNumbers.add(PhoneNumber(phoneNumber, DEFAULT_PHONE_NUMBER_TYPE, "")) + val action = intent.action + if (((contact!!.id == 0 && action == Intent.ACTION_INSERT) || action == ADD_NEW_CONTACT_NUMBER) && intent.extras != null) { + val phoneNumber = getPhoneNumberFromIntent(intent) + if (phoneNumber != null) { + contact!!.phoneNumbers.add(PhoneNumber(phoneNumber, DEFAULT_PHONE_NUMBER_TYPE, "", phoneNumber.normalizeNumber())) if (phoneNumber.isNotEmpty() && action == ADD_NEW_CONTACT_NUMBER) { highlightLastPhoneNumber = true } } - val firstName = intent.extras.get(KEY_NAME) + val firstName = intent.extras!!.get(KEY_NAME) if (firstName != null) { contact!!.firstName = firstName.toString() } - val data = intent.extras.getParcelableArrayList("data") + val data = intent.extras!!.getParcelableArrayList("data") if (data != null) { parseIntentData(data) } @@ -530,7 +544,7 @@ class EditContactActivity : ContactActivity() { applyColorFilter(getAdjustedPrimaryColor()) background.applyColorFilter(config.textColor) setOnClickListener { - removeGroup(group.id) + removeGroup(group.id!!) } } } @@ -561,9 +575,7 @@ class EditContactActivity : ContactActivity() { private fun setupNewContact() { supportActionBar?.title = resources.getString(R.string.new_contact) originalContactSource = if (hasContactPermissions()) config.lastUsedContactSource else SMT_PRIVATE - val organization = Organization("", "") - contact = Contact(0, "", "", "", "", "", "", "", ArrayList(), ArrayList(), ArrayList(), ArrayList(), originalContactSource, 0, 0, "", - null, "", ArrayList(), organization, ArrayList(), ArrayList(), ArrayList()) + contact = getEmptyContact() contact_source.text = getPublicContactSource(contact!!.source) } @@ -875,7 +887,7 @@ class EditContactActivity : ContactActivity() { val numberLabel = if (numberType == CommonDataKinds.Phone.TYPE_CUSTOM) numberHolder.contact_number_type.value else "" if (number.isNotEmpty()) { - phoneNumbers.add(PhoneNumber(number, numberType, numberLabel)) + phoneNumbers.add(PhoneNumber(number, numberType, numberLabel, number.normalizeNumber())) } } return phoneNumbers diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/GroupContactsActivity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/GroupContactsActivity.kt index 0e33c5c5..ab35f298 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/GroupContactsActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/GroupContactsActivity.kt @@ -70,8 +70,8 @@ class GroupContactsActivity : SimpleActivity(), RemoveFromGroupListener, Refresh private fun fabClicked() { SelectContactsDialog(this, allContacts, groupContacts) { addedContacts, removedContacts -> Thread { - addContactsToGroup(addedContacts, group.id) - removeContactsFromGroup(removedContacts, group.id) + addContactsToGroup(addedContacts, group.id!!) + removeContactsFromGroup(removedContacts, group.id!!) refreshContacts() }.start() } @@ -86,11 +86,6 @@ class GroupContactsActivity : SimpleActivity(), RemoveFromGroupListener, Refresh group_contacts_placeholder_2.beVisibleIf(groupContacts.isEmpty()) group_contacts_placeholder.beVisibleIf(groupContacts.isEmpty()) group_contacts_list.beVisibleIf(groupContacts.isNotEmpty()) - - Contact.sorting = config.sorting - Contact.startWithSurname = config.startNameWithSurname - groupContacts.sort() - updateContacts(groupContacts) } } @@ -129,8 +124,8 @@ class GroupContactsActivity : SimpleActivity(), RemoveFromGroupListener, Refresh override fun removeFromGroup(contacts: ArrayList) { Thread { - removeContactsFromGroup(contacts, group.id) - if (groupContacts.size == 0) { + removeContactsFromGroup(contacts, group.id!!) + if (groupContacts.size == contacts.size) { refreshContacts() } }.start() diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/InsertOrEditContactActivity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/InsertOrEditContactActivity.kt index b2303819..36934f94 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/InsertOrEditContactActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/InsertOrEditContactActivity.kt @@ -13,10 +13,7 @@ import com.simplemobiletools.contacts.pro.R import com.simplemobiletools.contacts.pro.adapters.ContactsAdapter import com.simplemobiletools.contacts.pro.extensions.config import com.simplemobiletools.contacts.pro.extensions.getContactPublicUri -import com.simplemobiletools.contacts.pro.helpers.ADD_NEW_CONTACT_NUMBER -import com.simplemobiletools.contacts.pro.helpers.ContactsHelper -import com.simplemobiletools.contacts.pro.helpers.KEY_PHONE -import com.simplemobiletools.contacts.pro.helpers.LOCATION_INSERT_OR_EDIT +import com.simplemobiletools.contacts.pro.helpers.* import com.simplemobiletools.contacts.pro.models.Contact import kotlinx.android.synthetic.main.activity_insert_edit_contact.* @@ -45,7 +42,7 @@ class InsertOrEditContactActivity : SimpleActivity() { Intent().apply { action = Intent.ACTION_INSERT data = ContactsContract.Contacts.CONTENT_URI - putExtra(KEY_PHONE, intent.getStringExtra(KEY_PHONE)) + putExtra(KEY_PHONE, getPhoneNumberFromIntent(intent)) if (resolveActivity(packageManager) != null) { startActivityForResult(this, START_INSERT_ACTIVITY) } else { @@ -58,15 +55,13 @@ class InsertOrEditContactActivity : SimpleActivity() { } private fun gotContacts(contacts: ArrayList) { - Contact.sorting = config.sorting - Contact.startWithSurname = config.startNameWithSurname - contacts.sort() - ContactsAdapter(this, contacts, null, LOCATION_INSERT_OR_EDIT, null, existing_contact_list, existing_contact_fastscroller) { + val contact = it as Contact Intent(applicationContext, EditContactActivity::class.java).apply { - data = getContactPublicUri(it as Contact) + data = getContactPublicUri(contact) action = ADD_NEW_CONTACT_NUMBER - putExtra(KEY_PHONE, intent.getStringExtra(KEY_PHONE)) + putExtra(KEY_PHONE, getPhoneNumberFromIntent(intent)) + putExtra(IS_PRIVATE, contact.isPrivate()) startActivityForResult(this, START_EDIT_ACTIVITY) } }.apply { diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/MainActivity.kt index f8eefe7d..ec08ce56 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/MainActivity.kt @@ -22,6 +22,7 @@ import com.simplemobiletools.commons.models.Release import com.simplemobiletools.contacts.pro.BuildConfig import com.simplemobiletools.contacts.pro.R import com.simplemobiletools.contacts.pro.adapters.ViewPagerAdapter +import com.simplemobiletools.contacts.pro.databases.ContactsDatabase import com.simplemobiletools.contacts.pro.dialogs.ChangeSortingDialog import com.simplemobiletools.contacts.pro.dialogs.ExportContactsDialog import com.simplemobiletools.contacts.pro.dialogs.FilterContactSourcesDialog @@ -39,6 +40,7 @@ import kotlinx.android.synthetic.main.fragment_groups.* import kotlinx.android.synthetic.main.fragment_recents.* import java.io.FileOutputStream + class MainActivity : SimpleActivity(), RefreshContactsListener { private var isSearchOpen = false private var searchMenuItem: MenuItem? = null @@ -158,11 +160,15 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { } } } - isFirstResume = false val dialpadIcon = resources.getColoredDrawableWithColor(R.drawable.ic_dialpad, if (isBlackAndWhiteTheme()) Color.BLACK else Color.WHITE) - main_dialpad_button.setImageDrawable(dialpadIcon) - main_dialpad_button.background.applyColorFilter(getAdjustedPrimaryColor()) + main_dialpad_button.apply { + setImageDrawable(dialpadIcon) + background.applyColorFilter(getAdjustedPrimaryColor()) + beVisibleIf(config.showDialpadButton) + } + + isFirstResume = false } override fun onPause() { @@ -173,6 +179,9 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { override fun onDestroy() { super.onDestroy() config.lastUsedViewPagerPage = viewpager.currentItem + if (!isChangingConfigurations) { + ContactsDatabase.destroyInstance() + } } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -362,7 +371,11 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { // selecting the proper tab sometimes glitches, add an extra selector to make sure we have it right main_tabs_holder.onGlobalLayout { Handler().postDelayed({ - main_tabs_holder.getTabAt(config.lastUsedViewPagerPage)?.select() + if (intent?.action == Intent.ACTION_VIEW && intent.type == "vnd.android.cursor.dir/calls") { + main_tabs_holder.getTabAt(getRecentsTabIndex())?.select() + } else { + main_tabs_holder.getTabAt(config.lastUsedViewPagerPage)?.select() + } invalidateOptionsMenu() }, 100L) } @@ -478,7 +491,7 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { } private fun launchAbout() { - val licenses = LICENSE_JODA or LICENSE_GLIDE or LICENSE_GSON or LICENSE_STETHO + val licenses = LICENSE_JODA or LICENSE_GLIDE or LICENSE_GSON val faqItems = arrayListOf( FAQItem(R.string.faq_1_title, R.string.faq_1_text), @@ -506,30 +519,39 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { return@getContacts } + val contacts = it if (refreshTabsMask and CONTACTS_TAB_MASK != 0) { - contacts_fragment?.refreshContacts(it) + contacts_fragment?.refreshContacts(contacts) } if (refreshTabsMask and FAVORITES_TAB_MASK != 0) { - favorites_fragment?.refreshContacts(it) + favorites_fragment?.refreshContacts(contacts) } if (refreshTabsMask and RECENTS_TAB_MASK != 0) { - recents_fragment?.refreshContacts(it) + recents_fragment?.refreshContacts(contacts) } if (refreshTabsMask and GROUPS_TAB_MASK != 0) { if (refreshTabsMask == GROUPS_TAB_MASK) { groups_fragment.skipHashComparing = true } - groups_fragment?.refreshContacts(it) + groups_fragment?.refreshContacts(contacts) } - } - if (refreshTabsMask and RECENTS_TAB_MASK != 0) { - ContactsHelper(this).getRecents { - runOnUiThread { - recents_fragment?.updateRecentCalls(it) + if (refreshTabsMask and RECENTS_TAB_MASK != 0) { + ContactsHelper(this).getRecents { + it.filter { it.name == null }.forEach { + val namelessCall = it + val contact = contacts.firstOrNull { it.doesContainPhoneNumber(namelessCall.number) } + if (contact != null) { + it.name = contact.getNameToDisplay() + } + } + + runOnUiThread { + recents_fragment?.updateRecentCalls(it) + } } } } @@ -537,6 +559,22 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { private fun getAllFragments() = arrayListOf(contacts_fragment, favorites_fragment, recents_fragment, groups_fragment) + private fun getRecentsTabIndex(): Int { + var index = 0 + if (config.showTabs and RECENTS_TAB_MASK == 0) { + return index + } + + if (config.showTabs and CONTACTS_TAB_MASK != 0) { + index++ + } + + if (config.showTabs and FAVORITES_TAB_MASK != 0) { + index++ + } + return index + } + private fun checkWhatsNewDialog() { arrayListOf().apply { add(Release(10, R.string.release_10)) diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/ManageBlockedNumbersActivity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/ManageBlockedNumbersActivity.kt new file mode 100644 index 00000000..ae1ed642 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/ManageBlockedNumbersActivity.kt @@ -0,0 +1,93 @@ +package com.simplemobiletools.contacts.pro.activities + +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import com.simplemobiletools.commons.extensions.beVisibleIf +import com.simplemobiletools.commons.extensions.getAdjustedPrimaryColor +import com.simplemobiletools.commons.extensions.underlineText +import com.simplemobiletools.commons.extensions.updateTextColors +import com.simplemobiletools.commons.interfaces.RefreshRecyclerViewListener +import com.simplemobiletools.contacts.pro.R +import com.simplemobiletools.contacts.pro.adapters.ManageBlockedNumbersAdapter +import com.simplemobiletools.contacts.pro.dialogs.AddBlockedNumberDialog +import com.simplemobiletools.contacts.pro.extensions.getBlockedNumbers +import com.simplemobiletools.contacts.pro.extensions.isDefaultDialer +import com.simplemobiletools.contacts.pro.helpers.REQUEST_CODE_SET_DEFAULT_DIALER +import com.simplemobiletools.contacts.pro.models.BlockedNumber +import kotlinx.android.synthetic.main.activity_manage_blocked_numbers.* + +class ManageBlockedNumbersActivity : SimpleActivity(), RefreshRecyclerViewListener { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_manage_blocked_numbers) + updateBlockedNumbers() + updateTextColors(manage_blocked_numbers_wrapper) + updatePlaceholderTexts() + + manage_blocked_numbers_placeholder_2.apply { + underlineText() + setTextColor(getAdjustedPrimaryColor()) + setOnClickListener { + if (isDefaultDialer()) { + addOrEditBlockedNumber() + } else { + launchSetDefaultDialerIntent() + } + } + } + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_add_blocked_number, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.add_blocked_number -> addOrEditBlockedNumber() + else -> return super.onOptionsItemSelected(item) + } + return true + } + + override fun refreshItems() { + updateBlockedNumbers() + } + + private fun updatePlaceholderTexts() { + manage_blocked_numbers_placeholder.text = getString(if (isDefaultDialer()) R.string.not_blocking_anyone else R.string.must_make_default_dialer) + manage_blocked_numbers_placeholder_2.text = getString(if (isDefaultDialer()) R.string.add_a_blocked_number else R.string.set_to_default) + } + + private fun updateBlockedNumbers() { + Thread { + val blockedNumbers = getBlockedNumbers() + runOnUiThread { + ManageBlockedNumbersAdapter(this, blockedNumbers, this, manage_blocked_numbers_list) { + addOrEditBlockedNumber(it as BlockedNumber) + }.apply { + manage_blocked_numbers_list.adapter = this + } + + manage_blocked_numbers_placeholder.beVisibleIf(blockedNumbers.isEmpty()) + manage_blocked_numbers_placeholder_2.beVisibleIf(blockedNumbers.isEmpty()) + } + }.start() + } + + private fun addOrEditBlockedNumber(currentNumber: BlockedNumber? = null) { + AddBlockedNumberDialog(this, currentNumber) { + updateBlockedNumbers() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) { + super.onActivityResult(requestCode, resultCode, resultData) + if (requestCode == REQUEST_CODE_SET_DEFAULT_DIALER && isDefaultDialer()) { + updatePlaceholderTexts() + updateBlockedNumbers() + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/SelectContactActivity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/SelectContactActivity.kt index d31c6462..ffeb168c 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/SelectContactActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/SelectContactActivity.kt @@ -17,7 +17,6 @@ import com.simplemobiletools.contacts.pro.extensions.config import com.simplemobiletools.contacts.pro.extensions.getContactPublicUri import com.simplemobiletools.contacts.pro.extensions.getVisibleContactSources import com.simplemobiletools.contacts.pro.helpers.ContactsHelper -import com.simplemobiletools.contacts.pro.helpers.SMT_PRIVATE import com.simplemobiletools.contacts.pro.models.Contact import kotlinx.android.synthetic.main.activity_select_contact.* @@ -90,7 +89,7 @@ class SelectContactActivity : SimpleActivity() { ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> it.phoneNumbers.isNotEmpty() else -> true } - it.source != SMT_PRIVATE && hasRequiredValues + !it.isPrivate() && hasRequiredValues } else { true } @@ -99,10 +98,6 @@ class SelectContactActivity : SimpleActivity() { val contactSources = getVisibleContactSources() contacts = contacts.filter { contactSources.contains(it.source) } as ArrayList - Contact.sorting = config.sorting - Contact.startWithSurname = config.startNameWithSurname - contacts.sort() - runOnUiThread { updatePlaceholderVisibility(contacts) SelectContactsAdapter(this, contacts, ArrayList(), false, select_contact_list) { diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/SettingsActivity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/SettingsActivity.kt index 0950612b..db351714 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/SettingsActivity.kt @@ -1,8 +1,13 @@ package com.simplemobiletools.contacts.pro.activities +import android.annotation.TargetApi +import android.content.Intent +import android.os.Build import android.os.Bundle import com.simplemobiletools.commons.dialogs.RadioGroupDialog -import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.extensions.beVisibleIf +import com.simplemobiletools.commons.extensions.updateTextColors +import com.simplemobiletools.commons.helpers.isNougatPlus import com.simplemobiletools.commons.models.RadioItem import com.simplemobiletools.contacts.pro.R import com.simplemobiletools.contacts.pro.dialogs.ManageVisibleFieldsDialog @@ -26,6 +31,7 @@ class SettingsActivity : SimpleActivity() { setupCustomizeColors() setupManageShownContactFields() setupManageShownTabs() + setupManageBlockedNumbers() setupUseEnglish() setupShowInfoBubble() setupShowContactThumbnails() @@ -35,6 +41,7 @@ class SettingsActivity : SimpleActivity() { setupUse24HourTimeFormat() setupFilterDuplicates() setupShowCallConfirmation() + setupShowDialpadButton() setupOnContactClick() updateTextColors(settings_holder) } @@ -57,6 +64,15 @@ class SettingsActivity : SimpleActivity() { } } + // support for device-wise blocking came on Android 7, rely only on that + @TargetApi(Build.VERSION_CODES.N) + private fun setupManageBlockedNumbers() { + settings_manage_blocked_numbers_holder.beVisibleIf(isNougatPlus()) + settings_manage_blocked_numbers_holder.setOnClickListener { + startActivity(Intent(this, ManageBlockedNumbersActivity::class.java)) + } + } + private fun setupUseEnglish() { settings_use_english_holder.beVisibleIf(config.wasUseEnglishToggled || Locale.getDefault().language != "en") settings_use_english.isChecked = config.useEnglish @@ -123,6 +139,14 @@ class SettingsActivity : SimpleActivity() { } } + private fun setupShowDialpadButton() { + settings_show_dialpad_button.isChecked = config.showDialpadButton + settings_show_dialpad_button_holder.setOnClickListener { + settings_show_dialpad_button.toggle() + config.showDialpadButton = settings_show_dialpad_button.isChecked + } + } + private fun setupOnContactClick() { settings_on_contact_click.text = getOnContactClickText() settings_on_contact_click_holder.setOnClickListener { diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/SimpleActivity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/SimpleActivity.kt index cff6e0a3..63ac285a 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/SimpleActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/SimpleActivity.kt @@ -1,7 +1,14 @@ package com.simplemobiletools.contacts.pro.activities +import android.annotation.TargetApi +import android.content.ContentValues +import android.content.Intent +import android.os.Build +import android.telecom.TelecomManager import com.simplemobiletools.commons.activities.BaseSimpleActivity import com.simplemobiletools.contacts.pro.R +import com.simplemobiletools.contacts.pro.helpers.KEY_PHONE +import com.simplemobiletools.contacts.pro.helpers.REQUEST_CODE_SET_DEFAULT_DIALER open class SimpleActivity : BaseSimpleActivity() { override fun getAppIconIDs() = arrayListOf( @@ -27,4 +34,27 @@ open class SimpleActivity : BaseSimpleActivity() { ) override fun getAppLauncherName() = getString(R.string.app_launcher_name) + + protected fun getPhoneNumberFromIntent(intent: Intent): String? { + if (intent.extras?.containsKey(KEY_PHONE) == true) { + return intent.getStringExtra(KEY_PHONE) + } else if (intent.extras?.containsKey("data") == true) { + // sample contact number from Google Contacts: + // data: [data1=+123 456 789 mimetype=vnd.android.cursor.item/phone_v2 _id=-1 data2=0] + val data = intent.extras!!.get("data") + if (data != null) { + val contentValues = (data as? ArrayList)?.firstOrNull() as? ContentValues + if (contentValues != null && contentValues.containsKey("data1")) { + return contentValues.getAsString("data1") + } + } + } + return null + } + + @TargetApi(Build.VERSION_CODES.M) + protected fun launchSetDefaultDialerIntent() { + val intent = Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER).putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME, packageName) + startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT_DIALER) + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/ViewContactActivity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/ViewContactActivity.kt index 386aaa2d..b1ef1744 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/ViewContactActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/activities/ViewContactActivity.kt @@ -6,6 +6,7 @@ import android.os.Bundle import android.provider.ContactsContract import android.view.Menu import android.view.MenuItem +import android.view.View import android.view.WindowManager import android.widget.RelativeLayout import com.simplemobiletools.commons.extensions.* @@ -25,6 +26,7 @@ import kotlinx.android.synthetic.main.item_website.view.* class ViewContactActivity : ContactActivity() { private var isViewIntent = false + private var wasEditLaunched = false private var showFields = 0 override fun onCreate(savedInstanceState: Bundle?) { @@ -39,21 +41,25 @@ class ViewContactActivity : ContactActivity() { if (isViewIntent) { handlePermission(PERMISSION_READ_CONTACTS) { if (it) { - initContact() + Thread { + initContact() + }.start() } else { toast(R.string.no_contacts_permission) finish() } } } else { - initContact() + Thread { + initContact() + }.start() } } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_view_contact, menu) menu.apply { - findItem(R.id.open_with).isVisible = contact?.source != SMT_PRIVATE + findItem(R.id.open_with).isVisible = contact?.isPrivate() == false } return true } @@ -64,7 +70,7 @@ class ViewContactActivity : ContactActivity() { } when (item.itemId) { - R.id.edit -> editContact(contact!!) + R.id.edit -> editContact() R.id.share -> shareContact() R.id.open_with -> openWith() R.id.delete -> deleteContact() @@ -100,19 +106,29 @@ class ViewContactActivity : ContactActivity() { if (contactId != 0 && !wasLookupKeyUsed) { contact = ContactsHelper(this).getContactWithId(contactId, intent.getBooleanExtra(IS_PRIVATE, false)) if (contact == null) { - toast(R.string.unknown_error_occurred) + if (!wasEditLaunched) { + toast(R.string.unknown_error_occurred) + } finish() - return + } else { + runOnUiThread { + gotContact() + } + } + } else { + if (contact == null) { + finish() + } else { + runOnUiThread { + gotContact() + } } } + } - if (contact == null) { - finish() - return - } - + private fun gotContact() { + contact_scrollview.beVisible() setupViewContact() - contact_send_sms.beVisibleIf(contact!!.phoneNumbers.isNotEmpty()) contact_start_call.beVisibleIf(contact!!.phoneNumbers.isNotEmpty()) contact_send_email.beVisibleIf(contact!!.emails.isNotEmpty()) @@ -163,6 +179,11 @@ class ViewContactActivity : ContactActivity() { setupContactSource() } + private fun editContact() { + wasEditLaunched = true + editContact(contact!!) + } + private fun openWith() { Intent().apply { action = ContactsContract.QuickContact.ACTION_QUICK_CONTACT @@ -191,21 +212,27 @@ class ViewContactActivity : ContactActivity() { contact!!.apply { contact_prefix.text = prefix contact_prefix.beVisibleIf(prefix.isNotEmpty() && showFields and SHOW_PREFIX_FIELD != 0) + contact_prefix.copyOnLongClick(prefix) contact_first_name.text = firstName contact_first_name.beVisibleIf(firstName.isNotEmpty() && showFields and SHOW_FIRST_NAME_FIELD != 0) + contact_first_name.copyOnLongClick(firstName) contact_middle_name.text = middleName contact_middle_name.beVisibleIf(middleName.isNotEmpty() && showFields and SHOW_MIDDLE_NAME_FIELD != 0) + contact_middle_name.copyOnLongClick(middleName) contact_surname.text = surname contact_surname.beVisibleIf(surname.isNotEmpty() && showFields and SHOW_SURNAME_FIELD != 0) + contact_surname.copyOnLongClick(surname) contact_suffix.text = suffix contact_suffix.beVisibleIf(suffix.isNotEmpty() && showFields and SHOW_SUFFIX_FIELD != 0) + contact_suffix.copyOnLongClick(suffix) contact_nickname.text = nickname contact_nickname.beVisibleIf(nickname.isNotEmpty() && showFields and SHOW_NICKNAME_FIELD != 0) + contact_nickname.copyOnLongClick(nickname) if (contact_prefix.isGone() && contact_first_name.isGone() && contact_middle_name.isGone() && contact_surname.isGone() && contact_suffix.isGone() && contact_nickname.isGone()) { @@ -225,6 +252,7 @@ class ViewContactActivity : ContactActivity() { contact_numbers_holder.addView(this) contact_number.text = phoneNumber.value contact_number_type.text = getPhoneNumberTypeText(phoneNumber.type, phoneNumber.label) + copyOnLongClick(phoneNumber.value) setOnClickListener { if (config.showCallConfirmation) { @@ -235,12 +263,6 @@ class ViewContactActivity : ContactActivity() { startCallIntent(phoneNumber.value) } } - - setOnLongClickListener { - copyToClipboard(phoneNumber.value) - toast(R.string.value_copied_to_clipboard) - true - } } } contact_numbers_image.beVisible() @@ -261,6 +283,7 @@ class ViewContactActivity : ContactActivity() { contact_emails_holder.addView(this) contact_email.text = email.value contact_email_type.text = getEmailTypeText(email.type, email.label) + copyOnLongClick(email.value) setOnClickListener { sendEmailIntent(email.value) @@ -285,6 +308,7 @@ class ViewContactActivity : ContactActivity() { contact_addresses_holder.addView(this) contact_address.text = address.value contact_address_type.text = getAddressTypeText(address.type, address.label) + copyOnLongClick(address.value) setOnClickListener { sendAddressIntent(address.value) @@ -309,6 +333,7 @@ class ViewContactActivity : ContactActivity() { contact_ims_holder.addView(this) contact_im.text = IM.value contact_im_type.text = getIMTypeText(IM.type, IM.label) + copyOnLongClick(IM.value) } } contact_ims_image.beVisible() @@ -330,6 +355,7 @@ class ViewContactActivity : ContactActivity() { it.value.getDateTimeFromDateString(contact_event) contact_event_type.setText(getEventTextId(it.type)) contact_event_remove.beGone() + copyOnLongClick(it.value) } } contact_events_image.beVisible() @@ -346,6 +372,7 @@ class ViewContactActivity : ContactActivity() { contact_notes.text = notes contact_notes_image.beVisible() contact_notes.beVisible() + contact_notes.copyOnLongClick(notes) } else { contact_notes_image.beGone() contact_notes.beGone() @@ -360,6 +387,8 @@ class ViewContactActivity : ContactActivity() { contact_organization_image.beGoneIf(organization.isEmpty()) contact_organization_company.beGoneIf(organization.company.isEmpty()) contact_organization_job_position.beGoneIf(organization.jobPosition.isEmpty()) + contact_organization_company.copyOnLongClick(contact_organization_company.value) + contact_organization_job_position.copyOnLongClick(contact_organization_job_position.value) if (organization.company.isEmpty() && organization.jobPosition.isNotEmpty()) { (contact_organization_image.layoutParams as RelativeLayout.LayoutParams).addRule(RelativeLayout.ALIGN_TOP, contact_organization_job_position.id) @@ -380,6 +409,7 @@ class ViewContactActivity : ContactActivity() { layoutInflater.inflate(R.layout.item_website, contact_websites_holder, false).apply { contact_websites_holder.addView(this) contact_website.text = url + copyOnLongClick(url) setOnClickListener { openWebsiteIntent(url) @@ -403,6 +433,7 @@ class ViewContactActivity : ContactActivity() { val group = it contact_groups_holder.addView(this) contact_group.text = group.title + copyOnLongClick(group.title) } } contact_groups_image.beVisible() @@ -415,9 +446,11 @@ class ViewContactActivity : ContactActivity() { private fun setupContactSource() { if (showFields and SHOW_CONTACT_SOURCE_FIELD != 0) { - contact_source.text = getPublicContactSource(contact!!.source) + val contactSourceValue = getPublicContactSource(contact!!.source) + contact_source.text = contactSourceValue contact_source_image.beVisible() contact_source.beVisible() + contact_source.copyOnLongClick(contactSourceValue) } else { contact_source_image.beGone() contact_source.beGone() @@ -425,4 +458,12 @@ class ViewContactActivity : ContactActivity() { } private fun getStarDrawable(on: Boolean) = resources.getDrawable(if (on) R.drawable.ic_star_on_big else R.drawable.ic_star_off_big) + + private fun View.copyOnLongClick(value: String) { + setOnLongClickListener { + copyToClipboard(value) + toast(R.string.value_copied_to_clipboard) + true + } + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/adapters/ContactsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/adapters/ContactsAdapter.kt index 5a4baf46..3ab8b592 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/adapters/ContactsAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/adapters/ContactsAdapter.kt @@ -34,6 +34,7 @@ class ContactsAdapter(activity: SimpleActivity, var contactItems: ArrayList Unit) : MyRecyclerViewAdapter(activity, recyclerView, fastScroller, itemClick) { + private val NEW_GROUP_ID = -1 private lateinit var contactDrawable: Drawable private lateinit var businessContactDrawable: Drawable @@ -158,7 +159,10 @@ class ContactsAdapter(activity: SimpleActivity, var contactItems: ArrayList() - ContactsHelper(activity).getStoredGroups().forEach { - items.add(RadioItem(it.id.toInt(), it.title)) + ContactsHelper(activity).getStoredGroups { + it.forEach { + items.add(RadioItem(it.id!!.toInt(), it.title)) + } + items.add(RadioItem(NEW_GROUP_ID, activity.getString(R.string.create_new_group))) + showGroupsPicker(items) } - items.add(RadioItem(NEW_GROUP_ID, activity.getString(R.string.create_new_group))) + } - RadioGroupDialog(activity, items, 0) { + private fun showGroupsPicker(radioItems: ArrayList) { + val selectedContacts = getSelectedItems() + RadioGroupDialog(activity, radioItems, 0) { if (it as Int == NEW_GROUP_ID) { CreateNewGroupDialog(activity) { Thread { - activity.addContactsToGroup(selectedContacts, it.id) + activity.addContactsToGroup(selectedContacts, it.id!!.toLong()) refreshListener?.refreshContacts(GROUPS_TAB_MASK) }.start() finishActMode() diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/adapters/GroupsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/adapters/GroupsAdapter.kt index f700168c..1d6f76d6 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/adapters/GroupsAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/adapters/GroupsAdapter.kt @@ -13,7 +13,7 @@ import com.simplemobiletools.contacts.pro.R import com.simplemobiletools.contacts.pro.activities.SimpleActivity import com.simplemobiletools.contacts.pro.dialogs.RenameGroupDialog import com.simplemobiletools.contacts.pro.extensions.config -import com.simplemobiletools.contacts.pro.extensions.dbHelper +import com.simplemobiletools.contacts.pro.extensions.groupsDB import com.simplemobiletools.contacts.pro.helpers.ContactsHelper import com.simplemobiletools.contacts.pro.helpers.GROUPS_TAB_MASK import com.simplemobiletools.contacts.pro.interfaces.RefreshContactsListener @@ -59,7 +59,7 @@ class GroupsAdapter(activity: SimpleActivity, var groups: ArrayList, val override fun getItemSelectionKey(position: Int) = groups.getOrNull(position)?.id?.toInt() - override fun getItemKeyPosition(key: Int) = groups.indexOfFirst { it.id.toInt() == key } + override fun getItemKeyPosition(key: Int) = groups.indexOfFirst { it.id!!.toInt() == key } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_group, parent) @@ -73,7 +73,7 @@ class GroupsAdapter(activity: SimpleActivity, var groups: ArrayList, val override fun getItemCount() = groups.size - private fun getItemWithKey(key: Int): Group? = groups.firstOrNull { it.id.toInt() == key } + private fun getItemWithKey(key: Int): Group? = groups.firstOrNull { it.id!!.toInt() == key } fun updateItems(newItems: ArrayList) { groups = newItems @@ -92,7 +92,9 @@ class GroupsAdapter(activity: SimpleActivity, var groups: ArrayList, val private fun askConfirmDelete() { ConfirmationDialog(activity) { - deleteGroups() + Thread { + deleteGroups() + }.start() } } @@ -101,28 +103,30 @@ class GroupsAdapter(activity: SimpleActivity, var groups: ArrayList, val return } - val groupsToRemove = groups.filter { selectedKeys.contains(it.id.toInt()) } as ArrayList + val groupsToRemove = groups.filter { selectedKeys.contains(it.id!!.toInt()) } as ArrayList val positions = getSelectedItemPositions() groupsToRemove.forEach { if (it.isPrivateSecretGroup()) { - activity.dbHelper.deleteGroup(it.id) + activity.groupsDB.deleteGroupId(it.id!!) } else { - ContactsHelper(activity).deleteGroup(it.id) + ContactsHelper(activity).deleteGroup(it.id!!) } } groups.removeAll(groupsToRemove) - if (groups.isEmpty()) { - refreshListener?.refreshContacts(GROUPS_TAB_MASK) - finishActMode() - } else { - removeSelectedItems(positions) + activity.runOnUiThread { + if (groups.isEmpty()) { + refreshListener?.refreshContacts(GROUPS_TAB_MASK) + finishActMode() + } else { + removeSelectedItems(positions) + } } } private fun setupView(view: View, group: Group) { view.apply { - group_frame?.isSelected = selectedKeys.contains(group.id.toInt()) + group_frame?.isSelected = selectedKeys.contains(group.id!!.toInt()) group_name.apply { setTextColor(textColor) text = String.format(activity.getString(R.string.groups_placeholder), group.title, group.contactsCount.toString()) diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/adapters/ManageBlockedNumbersAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/adapters/ManageBlockedNumbersAdapter.kt new file mode 100644 index 00000000..5dee5305 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/adapters/ManageBlockedNumbersAdapter.kt @@ -0,0 +1,83 @@ +package com.simplemobiletools.contacts.pro.adapters + +import android.view.Menu +import android.view.View +import android.view.ViewGroup +import com.simplemobiletools.commons.activities.BaseSimpleActivity +import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter +import com.simplemobiletools.commons.interfaces.RefreshRecyclerViewListener +import com.simplemobiletools.commons.views.MyRecyclerView +import com.simplemobiletools.contacts.pro.R +import com.simplemobiletools.contacts.pro.extensions.config +import com.simplemobiletools.contacts.pro.extensions.deleteBlockedNumber +import com.simplemobiletools.contacts.pro.models.BlockedNumber +import kotlinx.android.synthetic.main.item_manage_blocked_number.view.* +import java.util.* + +class ManageBlockedNumbersAdapter(activity: BaseSimpleActivity, var blockedNumbers: ArrayList, val listener: RefreshRecyclerViewListener?, + recyclerView: MyRecyclerView, itemClick: (Any) -> Unit) : MyRecyclerViewAdapter(activity, recyclerView, null, itemClick) { + + private val config = activity.config + + init { + setupDragListener(true) + } + + override fun getActionMenuId() = R.menu.cab_remove_only + + override fun prepareActionMode(menu: Menu) {} + + override fun actionItemPressed(id: Int) { + when (id) { + R.id.cab_remove -> removeSelection() + } + } + + override fun getSelectableItemCount() = blockedNumbers.size + + override fun getIsItemSelectable(position: Int) = true + + override fun getItemSelectionKey(position: Int) = blockedNumbers.getOrNull(position)?.id?.toInt() + + override fun getItemKeyPosition(key: Int) = blockedNumbers.indexOfFirst { it.id.toInt() == key } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_manage_blocked_number, parent) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val blockedNumber = blockedNumbers[position] + holder.bindView(blockedNumber, true, true) { itemView, adapterPosition -> + setupView(itemView, blockedNumber) + } + bindViewHolder(holder) + } + + override fun getItemCount() = blockedNumbers.size + + private fun getSelectedItems() = blockedNumbers.filter { selectedKeys.contains(it.id.toInt()) } as ArrayList + + private fun setupView(view: View, blockedNumber: BlockedNumber) { + view.apply { + manage_blocked_number_holder?.isSelected = selectedKeys.contains(blockedNumber.id.toInt()) + manage_blocked_number_title.apply { + text = blockedNumber.number + setTextColor(config.textColor) + } + } + } + + private fun removeSelection() { + val removeBlockedNumbers = ArrayList(selectedKeys.size) + val positions = getSelectedItemPositions() + + getSelectedItems().forEach { + removeBlockedNumbers.add(it) + activity.deleteBlockedNumber(it.number) + } + + blockedNumbers.removeAll(removeBlockedNumbers) + removeSelectedItems(positions) + if (blockedNumbers.isEmpty()) { + listener?.refreshItems() + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/adapters/RecentCallsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/adapters/RecentCallsAdapter.kt index 5a871563..41b24af4 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/adapters/RecentCallsAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/adapters/RecentCallsAdapter.kt @@ -6,10 +6,12 @@ import android.view.ViewGroup import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter import com.simplemobiletools.commons.dialogs.ConfirmationDialog import com.simplemobiletools.commons.extensions.beVisibleIf +import com.simplemobiletools.commons.helpers.isNougatPlus import com.simplemobiletools.commons.views.FastScroller import com.simplemobiletools.commons.views.MyRecyclerView import com.simplemobiletools.contacts.pro.R import com.simplemobiletools.contacts.pro.activities.SimpleActivity +import com.simplemobiletools.contacts.pro.extensions.addBlockedNumber import com.simplemobiletools.contacts.pro.extensions.config import com.simplemobiletools.contacts.pro.helpers.ContactsHelper import com.simplemobiletools.contacts.pro.helpers.RECENTS_TAB_MASK @@ -28,7 +30,12 @@ class RecentCallsAdapter(activity: SimpleActivity, var recentCalls: ArrayList selectAll() R.id.cab_delete -> askConfirmDelete() + R.id.cab_block_number -> blockNumber() } } @@ -92,6 +100,19 @@ class RecentCallsAdapter(activity: SimpleActivity, var recentCalls: ArrayList private fun setupView(view: View, recentCall: RecentCall) { diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/databases/ContactsDatabase.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/databases/ContactsDatabase.kt new file mode 100644 index 00000000..530b1cde --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/databases/ContactsDatabase.kt @@ -0,0 +1,71 @@ +package com.simplemobiletools.contacts.pro.databases + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import androidx.sqlite.db.SupportSQLiteDatabase +import com.simplemobiletools.contacts.pro.helpers.Converters +import com.simplemobiletools.contacts.pro.helpers.FIRST_CONTACT_ID +import com.simplemobiletools.contacts.pro.helpers.FIRST_GROUP_ID +import com.simplemobiletools.contacts.pro.helpers.getEmptyLocalContact +import com.simplemobiletools.contacts.pro.interfaces.ContactsDao +import com.simplemobiletools.contacts.pro.interfaces.GroupsDao +import com.simplemobiletools.contacts.pro.models.Group +import com.simplemobiletools.contacts.pro.models.LocalContact +import java.util.concurrent.Executors + +@Database(entities = [LocalContact::class, Group::class], version = 1) +@TypeConverters(Converters::class) +abstract class ContactsDatabase : RoomDatabase() { + + abstract fun ContactsDao(): ContactsDao + + abstract fun GroupsDao(): GroupsDao + + companion object { + private var db: ContactsDatabase? = null + + fun getInstance(context: Context): ContactsDatabase { + if (db == null) { + synchronized(ContactsDatabase::class) { + if (db == null) { + db = Room.databaseBuilder(context.applicationContext, ContactsDatabase::class.java, "local_contacts.db") + .addCallback(object : Callback() { + override fun onCreate(db: SupportSQLiteDatabase) { + super.onCreate(db) + increaseAutoIncrementIds() + } + }) + .build() + } + } + } + return db!! + } + + fun destroyInstance() { + db = null + } + + // start autoincrement ID from FIRST_CONTACT_ID/FIRST_GROUP_ID to avoid conflicts + // Room doesn't seem to have a built in way for it, so just create a contact/group and delete it + private fun increaseAutoIncrementIds() { + Executors.newSingleThreadExecutor().execute { + val emptyContact = getEmptyLocalContact() + emptyContact.id = FIRST_CONTACT_ID + db!!.ContactsDao().apply { + insertOrUpdate(emptyContact) + deleteContactId(FIRST_CONTACT_ID) + } + + val emptyGroup = Group(FIRST_GROUP_ID, "") + db!!.GroupsDao().apply { + insertOrUpdate(emptyGroup) + deleteGroupId(FIRST_GROUP_ID) + } + } + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/AddBlockedNumberDialog.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/AddBlockedNumberDialog.kt new file mode 100644 index 00000000..32a9497f --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/AddBlockedNumberDialog.kt @@ -0,0 +1,44 @@ +package com.simplemobiletools.contacts.pro.dialogs + +import androidx.appcompat.app.AlertDialog +import com.simplemobiletools.commons.activities.BaseSimpleActivity +import com.simplemobiletools.commons.extensions.setupDialogStuff +import com.simplemobiletools.commons.extensions.showKeyboard +import com.simplemobiletools.commons.extensions.value +import com.simplemobiletools.contacts.pro.R +import com.simplemobiletools.contacts.pro.extensions.addBlockedNumber +import com.simplemobiletools.contacts.pro.extensions.deleteBlockedNumber +import com.simplemobiletools.contacts.pro.models.BlockedNumber +import kotlinx.android.synthetic.main.dialog_add_blocked_number.view.* + +class AddBlockedNumberDialog(val activity: BaseSimpleActivity, val originalNumber: BlockedNumber? = null, val callback: () -> Unit) { + init { + val view = activity.layoutInflater.inflate(R.layout.dialog_add_blocked_number, null).apply { + if (originalNumber != null) { + add_blocked_number_edittext.setText(originalNumber.number) + } + } + + AlertDialog.Builder(activity) + .setPositiveButton(R.string.ok, null) + .setNegativeButton(R.string.cancel, null) + .create().apply { + activity.setupDialogStuff(view, this) { + showKeyboard(view.add_blocked_number_edittext) + getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + val newBlockedNumber = view.add_blocked_number_edittext.value + if (originalNumber != null && newBlockedNumber != originalNumber.number) { + activity.deleteBlockedNumber(originalNumber.number) + } + + if (newBlockedNumber.isNotEmpty()) { + activity.addBlockedNumber(newBlockedNumber) + } + + callback() + dismiss() + } + } + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/CreateNewGroupDialog.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/CreateNewGroupDialog.kt index f4eb3dbb..a0e0e9e8 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/CreateNewGroupDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/CreateNewGroupDialog.kt @@ -65,10 +65,14 @@ class CreateNewGroupDialog(val activity: BaseSimpleActivity, val callback: (newG } private fun createGroupUnder(name: String, contactSource: ContactSource, dialog: AlertDialog) { - val newGroup = ContactsHelper(activity).createNewGroup(name, contactSource.name, contactSource.type) - if (newGroup != null) { - callback(newGroup) - } - dialog.dismiss() + Thread { + val newGroup = ContactsHelper(activity).createNewGroup(name, contactSource.name, contactSource.type) + activity.runOnUiThread { + if (newGroup != null) { + callback(newGroup) + } + dialog.dismiss() + } + }.start() } } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/RenameGroupDialog.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/RenameGroupDialog.kt index ea9180fb..835bc5e5 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/RenameGroupDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/RenameGroupDialog.kt @@ -4,7 +4,7 @@ import androidx.appcompat.app.AlertDialog import com.simplemobiletools.commons.activities.BaseSimpleActivity import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.contacts.pro.R -import com.simplemobiletools.contacts.pro.extensions.dbHelper +import com.simplemobiletools.contacts.pro.extensions.groupsDB import com.simplemobiletools.contacts.pro.helpers.ContactsHelper import com.simplemobiletools.contacts.pro.models.Group import kotlinx.android.synthetic.main.dialog_rename_group.view.* @@ -35,13 +35,17 @@ class RenameGroupDialog(val activity: BaseSimpleActivity, val group: Group, val } group.title = newTitle - if (group.isPrivateSecretGroup()) { - activity.dbHelper.renameGroup(group) - } else { - ContactsHelper(activity).renameGroup(group) - } - callback() - dismiss() + Thread { + if (group.isPrivateSecretGroup()) { + activity.groupsDB.insertOrUpdate(group) + } else { + ContactsHelper(activity).renameGroup(group) + } + activity.runOnUiThread { + callback() + dismiss() + } + }.start() } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/SelectContactsDialog.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/SelectContactsDialog.kt index b01bfacd..16872932 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/SelectContactsDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/SelectContactsDialog.kt @@ -6,12 +6,11 @@ import com.simplemobiletools.commons.extensions.setupDialogStuff import com.simplemobiletools.contacts.pro.R import com.simplemobiletools.contacts.pro.activities.SimpleActivity import com.simplemobiletools.contacts.pro.adapters.SelectContactsAdapter -import com.simplemobiletools.contacts.pro.extensions.config import com.simplemobiletools.contacts.pro.extensions.getVisibleContactSources import com.simplemobiletools.contacts.pro.models.Contact import kotlinx.android.synthetic.main.layout_select_contact.view.* -class SelectContactsDialog(val activity: SimpleActivity, initialContacts: ArrayList, val selectContacts: ArrayList? = null, +class SelectContactsDialog(val activity: SimpleActivity, initialContacts: ArrayList, selectContacts: ArrayList? = null, val callback: (addedContacts: ArrayList, removedContacts: ArrayList) -> Unit) { private var view = activity.layoutInflater.inflate(R.layout.layout_select_contact, null) private var initiallySelectedContacts = ArrayList() @@ -27,10 +26,6 @@ class SelectContactsDialog(val activity: SimpleActivity, initialContacts: ArrayL initiallySelectedContacts = selectContacts } - Contact.sorting = activity.config.sorting - Contact.startWithSurname = activity.config.startNameWithSurname - allContacts.sort() - activity.runOnUiThread { view.apply { select_contact_list.adapter = SelectContactsAdapter(activity, allContacts, initiallySelectedContacts, true, select_contact_list) diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/SelectGroupsDialog.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/SelectGroupsDialog.kt index 78a35542..25e97f5b 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/SelectGroupsDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/SelectGroupsDialog.kt @@ -17,11 +17,20 @@ import java.util.* class SelectGroupsDialog(val activity: SimpleActivity, val selectedGroups: ArrayList, val callback: (newGroups: ArrayList) -> Unit) { private val view = activity.layoutInflater.inflate(R.layout.dialog_select_groups, null) as ViewGroup private val checkboxes = ArrayList() - private val groups = ContactsHelper(activity).getStoredGroups() + private var groups = ArrayList() private val config = activity.config - private val dialog: AlertDialog? + private var dialog: AlertDialog? = null init { + ContactsHelper(activity).getStoredGroups { + groups = it + activity.runOnUiThread { + initDialog() + } + } + } + + private fun initDialog() { groups.sortedBy { it.title }.forEach { addGroupCheckbox(it) } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/extensions/Activity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/extensions/Activity.kt index 8fddb344..cda4b77a 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/extensions/Activity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/extensions/Activity.kt @@ -1,9 +1,7 @@ package com.simplemobiletools.contacts.pro.extensions -import android.app.Activity import android.content.Intent import android.net.Uri -import android.provider.ContactsContract import com.simplemobiletools.commons.activities.BaseSimpleActivity import com.simplemobiletools.commons.dialogs.RadioGroupDialog import com.simplemobiletools.commons.extensions.sharePathIntent @@ -17,8 +15,6 @@ import com.simplemobiletools.contacts.pro.activities.SimpleActivity import com.simplemobiletools.contacts.pro.dialogs.CallConfirmationDialog import com.simplemobiletools.contacts.pro.helpers.* import com.simplemobiletools.contacts.pro.models.Contact -import com.simplemobiletools.contacts.pro.models.ContactSource -import java.io.File fun SimpleActivity.startCallIntent(recipient: String) { handlePermission(PERMISSION_CALL_PHONE) { @@ -93,13 +89,6 @@ fun SimpleActivity.showContactSourcePicker(currentSource: String, callback: (new } } -fun SimpleActivity.getPublicContactSource(source: String): String { - return when (source) { - config.localAccountName -> getString(R.string.phone_storage) - SMT_PRIVATE -> getString(R.string.phone_storage_hidden) - else -> source - } -} fun BaseSimpleActivity.shareContacts(contacts: ArrayList) { val file = getTempFile() @@ -117,96 +106,6 @@ fun BaseSimpleActivity.shareContacts(contacts: ArrayList) { } } -fun BaseSimpleActivity.sendSMSToContacts(contacts: ArrayList) { - val numbers = StringBuilder() - contacts.forEach { - val number = it.phoneNumbers.firstOrNull { it.type == ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE } - ?: it.phoneNumbers.firstOrNull() - if (number != null) { - numbers.append("${number.value};") - } - - val uriString = "smsto:${numbers.toString().trimEnd(';')}" - Intent(Intent.ACTION_SENDTO, Uri.parse(uriString)).apply { - if (resolveActivity(packageManager) != null) { - startActivity(this) - } else { - toast(R.string.no_app_found) - } - } - } -} - -fun BaseSimpleActivity.sendEmailToContacts(contacts: ArrayList) { - val emails = ArrayList() - contacts.forEach { - it.emails.forEach { - if (it.value.isNotEmpty()) { - emails.add(it.value) - } - } - } - - Intent(Intent.ACTION_SEND_MULTIPLE).apply { - type = "message/rfc822" - putExtra(Intent.EXTRA_EMAIL, emails.toTypedArray()) - if (resolveActivity(packageManager) != null) { - startActivity(this) - } else { - toast(R.string.no_app_found) - } - } -} - -fun BaseSimpleActivity.getTempFile(): File? { - val folder = File(cacheDir, "contacts") - if (!folder.exists()) { - if (!folder.mkdir()) { - toast(R.string.unknown_error_occurred) - return null - } - } - - return File(folder, "contacts.vcf") -} - -fun BaseSimpleActivity.addContactsToGroup(contacts: ArrayList, groupId: Long) { - val publicContacts = contacts.filter { it.source != SMT_PRIVATE } - val privateContacts = contacts.filter { it.source == SMT_PRIVATE } - if (publicContacts.isNotEmpty()) { - ContactsHelper(this).addContactsToGroup(contacts, groupId) - } - - if (privateContacts.isNotEmpty()) { - dbHelper.addContactsToGroup(contacts, groupId) - } -} - -fun BaseSimpleActivity.removeContactsFromGroup(contacts: ArrayList, groupId: Long) { - val publicContacts = contacts.filter { it.source != SMT_PRIVATE } - val privateContacts = contacts.filter { it.source == SMT_PRIVATE } - if (publicContacts.isNotEmpty() && hasContactPermissions()) { - ContactsHelper(this).removeContactsFromGroup(contacts, groupId) - } - - if (privateContacts.isNotEmpty()) { - dbHelper.removeContactsFromGroup(contacts, groupId) - } -} - -fun BaseSimpleActivity.getContactPublicUri(contact: Contact): Uri { - val lookupKey = ContactsHelper(this).getContactLookupKey(contact.id.toString()) - return Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, lookupKey) -} - -fun Activity.getVisibleContactSources(): ArrayList { - val sources = ContactsHelper(this).getDeviceContactSources() - sources.add(ContactSource(getString(R.string.phone_storage_hidden), SMT_PRIVATE)) - val sourceNames = ArrayList(sources).map { if (it.type == SMT_PRIVATE) SMT_PRIVATE else it.name }.toMutableList() as ArrayList - sourceNames.removeAll(config.ignoredContactSources) - return sourceNames -} - fun SimpleActivity.contactClicked(contact: Contact) { when (config.onContactClick) { ON_CLICK_CALL_CONTACT -> callContact(contact) diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/extensions/Context.kt index 0726550a..0b42a0aa 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/extensions/Context.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/extensions/Context.kt @@ -1,32 +1,55 @@ package com.simplemobiletools.contacts.pro.extensions +import android.annotation.TargetApi +import android.content.ContentValues import android.content.Context import android.content.Intent import android.database.Cursor import android.net.Uri +import android.os.Build +import android.provider.BlockedNumberContract +import android.provider.BlockedNumberContract.BlockedNumbers import android.provider.ContactsContract +import android.telecom.TelecomManager import androidx.core.content.FileProvider -import com.simplemobiletools.commons.extensions.getIntValue -import com.simplemobiletools.commons.extensions.hasPermission -import com.simplemobiletools.commons.extensions.toast +import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.PERMISSION_READ_CONTACTS import com.simplemobiletools.commons.helpers.PERMISSION_WRITE_CONTACTS +import com.simplemobiletools.commons.helpers.isNougatPlus import com.simplemobiletools.contacts.pro.BuildConfig import com.simplemobiletools.contacts.pro.R import com.simplemobiletools.contacts.pro.activities.EditContactActivity import com.simplemobiletools.contacts.pro.activities.ViewContactActivity +import com.simplemobiletools.contacts.pro.databases.ContactsDatabase import com.simplemobiletools.contacts.pro.helpers.* +import com.simplemobiletools.contacts.pro.interfaces.ContactsDao +import com.simplemobiletools.contacts.pro.interfaces.GroupsDao +import com.simplemobiletools.contacts.pro.models.BlockedNumber import com.simplemobiletools.contacts.pro.models.Contact +import com.simplemobiletools.contacts.pro.models.ContactSource +import com.simplemobiletools.contacts.pro.models.Organization import java.io.File + val Context.config: Config get() = Config.newInstance(applicationContext) -val Context.dbHelper: DBHelper get() = DBHelper.newInstance(applicationContext) +val Context.contactsDB: ContactsDao get() = ContactsDatabase.getInstance(applicationContext).ContactsDao() + +val Context.groupsDB: GroupsDao get() = ContactsDatabase.getInstance(applicationContext).GroupsDao() + +val Context.telecomManager: TelecomManager get() = getSystemService(Context.TELECOM_SERVICE) as TelecomManager + +fun Context.getEmptyContact(): Contact { + val originalContactSource = if (hasContactPermissions()) config.lastUsedContactSource else SMT_PRIVATE + val organization = Organization("", "") + return Contact(0, "", "", "", "", "", "", "", ArrayList(), ArrayList(), ArrayList(), ArrayList(), originalContactSource, 0, 0, "", + null, "", ArrayList(), organization, ArrayList(), ArrayList()) +} fun Context.viewContact(contact: Contact) { Intent(applicationContext, ViewContactActivity::class.java).apply { putExtra(CONTACT_ID, contact.id) - putExtra(IS_PRIVATE, contact.source == SMT_PRIVATE) + putExtra(IS_PRIVATE, contact.isPrivate()) startActivity(this) } } @@ -34,7 +57,7 @@ fun Context.viewContact(contact: Contact) { fun Context.editContact(contact: Contact) { Intent(applicationContext, EditContactActivity::class.java).apply { putExtra(CONTACT_ID, contact.id) - putExtra(IS_PRIVATE, contact.source == SMT_PRIVATE) + putExtra(IS_PRIVATE, contact.isPrivate()) startActivity(this) } } @@ -168,3 +191,157 @@ fun Context.getPhotoThumbnailSize(): Int { } fun Context.hasContactPermissions() = hasPermission(PERMISSION_READ_CONTACTS) && hasPermission(PERMISSION_WRITE_CONTACTS) + +fun Context.getPublicContactSource(source: String): String { + return when (source) { + config.localAccountName -> getString(R.string.phone_storage) + SMT_PRIVATE -> getString(R.string.phone_storage_hidden) + else -> source + } +} + +fun Context.sendSMSToContacts(contacts: ArrayList) { + val numbers = StringBuilder() + contacts.forEach { + val number = it.phoneNumbers.firstOrNull { it.type == ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE } + ?: it.phoneNumbers.firstOrNull() + if (number != null) { + numbers.append("${number.value};") + } + + val uriString = "smsto:${numbers.toString().trimEnd(';')}" + Intent(Intent.ACTION_SENDTO, Uri.parse(uriString)).apply { + if (resolveActivity(packageManager) != null) { + startActivity(this) + } else { + toast(R.string.no_app_found) + } + } + } +} + +fun Context.sendEmailToContacts(contacts: ArrayList) { + val emails = ArrayList() + contacts.forEach { + it.emails.forEach { + if (it.value.isNotEmpty()) { + emails.add(it.value) + } + } + } + + Intent(Intent.ACTION_SEND_MULTIPLE).apply { + type = "message/rfc822" + putExtra(Intent.EXTRA_EMAIL, emails.toTypedArray()) + if (resolveActivity(packageManager) != null) { + startActivity(this) + } else { + toast(R.string.no_app_found) + } + } +} + +fun Context.getTempFile(): File? { + val folder = File(cacheDir, "contacts") + if (!folder.exists()) { + if (!folder.mkdir()) { + toast(R.string.unknown_error_occurred) + return null + } + } + + return File(folder, "contacts.vcf") +} + +fun Context.addContactsToGroup(contacts: ArrayList, groupId: Long) { + val publicContacts = contacts.filter { !it.isPrivate() }.toMutableList() as ArrayList + val privateContacts = contacts.filter { it.isPrivate() }.toMutableList() as ArrayList + if (publicContacts.isNotEmpty()) { + ContactsHelper(this).addContactsToGroup(publicContacts, groupId) + } + + if (privateContacts.isNotEmpty()) { + LocalContactsHelper(this).addContactsToGroup(privateContacts, groupId) + } +} + +fun Context.removeContactsFromGroup(contacts: ArrayList, groupId: Long) { + val publicContacts = contacts.filter { !it.isPrivate() }.toMutableList() as ArrayList + val privateContacts = contacts.filter { it.isPrivate() }.toMutableList() as ArrayList + if (publicContacts.isNotEmpty() && hasContactPermissions()) { + ContactsHelper(this).removeContactsFromGroup(publicContacts, groupId) + } + + if (privateContacts.isNotEmpty()) { + LocalContactsHelper(this).removeContactsFromGroup(privateContacts, groupId) + } +} + +fun Context.getContactPublicUri(contact: Contact): Uri { + val lookupKey = if (contact.isPrivate()) { + "local_${contact.id}" + } else { + ContactsHelper(this).getContactLookupKey(contact.id.toString()) + } + return Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, lookupKey) +} + +fun Context.getVisibleContactSources(): ArrayList { + val sources = ContactsHelper(this).getDeviceContactSources() + sources.add(ContactSource(getString(R.string.phone_storage_hidden), SMT_PRIVATE)) + val sourceNames = ArrayList(sources).map { if (it.type == SMT_PRIVATE) SMT_PRIVATE else it.name }.toMutableList() as ArrayList + sourceNames.removeAll(config.ignoredContactSources) + return sourceNames +} + +@TargetApi(Build.VERSION_CODES.N) +fun Context.getBlockedNumbers(): ArrayList { + val blockedNumbers = ArrayList() + if (!isNougatPlus() || !isDefaultDialer()) { + return blockedNumbers + } + + val uri = BlockedNumberContract.BlockedNumbers.CONTENT_URI + val projection = arrayOf( + BlockedNumberContract.BlockedNumbers.COLUMN_ID, + BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER, + BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER + ) + + var cursor: Cursor? = null + try { + cursor = contentResolver.query(uri, projection, null, null, null) + if (cursor?.moveToFirst() == true) { + do { + val id = cursor.getLongValue(BlockedNumberContract.BlockedNumbers.COLUMN_ID) + val number = cursor.getStringValue(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER) ?: "" + val normalizedNumber = cursor.getStringValue(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER) ?: "" + val blockedNumber = BlockedNumber(id, number, normalizedNumber) + blockedNumbers.add(blockedNumber) + } while (cursor.moveToNext()) + } + } finally { + cursor?.close() + } + + return blockedNumbers +} + +@TargetApi(Build.VERSION_CODES.N) +fun Context.addBlockedNumber(number: String) { + ContentValues().apply { + put(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, number) + contentResolver.insert(BlockedNumbers.CONTENT_URI, this) + } +} + +@TargetApi(Build.VERSION_CODES.N) +fun Context.deleteBlockedNumber(number: String) { + val values = ContentValues() + values.put(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, number) + val uri = contentResolver.insert(BlockedNumbers.CONTENT_URI, values) + contentResolver.delete(uri, null, null) +} + +@TargetApi(Build.VERSION_CODES.M) +fun Context.isDefaultDialer() = telecomManager.defaultDialerPackage == packageName diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/extensions/String.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/extensions/String.kt index b35d3b9a..beda894f 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/extensions/String.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/extensions/String.kt @@ -1,8 +1,8 @@ package com.simplemobiletools.contacts.pro.extensions +import android.telephony.PhoneNumberUtils import android.widget.TextView import com.simplemobiletools.commons.helpers.getDateFormats -import com.simplemobiletools.contacts.pro.helpers.PHONE_NUMBER_PATTERN import org.joda.time.DateTime import org.joda.time.format.DateTimeFormat import java.text.DateFormat @@ -34,4 +34,4 @@ fun String.getDateTimeFromDateString(viewToUpdate: TextView? = null): DateTime { return date } -fun String.applyRegexFiltering() = replace(PHONE_NUMBER_PATTERN.toRegex(), "") +fun String.normalizeNumber() = PhoneNumberUtils.normalizeNumber(this) diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/fragments/MyViewPagerFragment.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/fragments/MyViewPagerFragment.kt index c35c676d..6421f519 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/fragments/MyViewPagerFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/fragments/MyViewPagerFragment.kt @@ -61,6 +61,7 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) } this is RecentsFragment -> { fragment_fab.beGone() + fragment_placeholder.text = activity.getString(R.string.no_recent_calls_found) fragment_placeholder_2.text = activity.getString(R.string.request_the_required_permissions) } } @@ -108,9 +109,6 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) config.lastUsedContactSource = grouped?.key ?: "" } - Contact.sorting = config.sorting - Contact.startWithSurname = config.startNameWithSurname - contacts.sort() allContacts = contacts val filtered = when { @@ -145,42 +143,44 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) } private fun setupGroupsAdapter(contacts: ArrayList) { - var storedGroups = ContactsHelper(activity!!).getStoredGroups() - contacts.forEach { - it.groups.forEach { - val group = it - val storedGroup = storedGroups.firstOrNull { it.id == group.id } - storedGroup?.addContact() - } - } - - storedGroups = storedGroups.asSequence().sortedWith(compareBy { it.title.normalizeString() }).toMutableList() as ArrayList - - fragment_placeholder_2.beVisibleIf(storedGroups.isEmpty()) - fragment_placeholder.beVisibleIf(storedGroups.isEmpty()) - fragment_list.beVisibleIf(storedGroups.isNotEmpty()) - - val currAdapter = fragment_list.adapter - if (currAdapter == null) { - GroupsAdapter(activity as SimpleActivity, storedGroups, activity, fragment_list, fragment_fastscroller) { - Intent(activity, GroupContactsActivity::class.java).apply { - putExtra(GROUP, it as Group) - activity!!.startActivity(this) + ContactsHelper(activity!!).getStoredGroups { + var storedGroups = it + contacts.forEach { + it.groups.forEach { + val group = it + val storedGroup = storedGroups.firstOrNull { it.id == group.id } + storedGroup?.addContact() } - }.apply { - addVerticalDividers(true) - fragment_list.adapter = this } - fragment_fastscroller.setScrollToY(0) - fragment_fastscroller.setViews(fragment_list) { - val item = (fragment_list.adapter as GroupsAdapter).groups.getOrNull(it) - fragment_fastscroller.updateBubbleText(item?.getBubbleText() ?: "") - } - } else { - (currAdapter as GroupsAdapter).apply { - showContactThumbnails = activity.config.showContactThumbnails - updateItems(storedGroups) + storedGroups = storedGroups.asSequence().sortedWith(compareBy { it.title.normalizeString() }).toMutableList() as ArrayList + + fragment_placeholder_2.beVisibleIf(storedGroups.isEmpty()) + fragment_placeholder.beVisibleIf(storedGroups.isEmpty()) + fragment_list.beVisibleIf(storedGroups.isNotEmpty()) + + val currAdapter = fragment_list.adapter + if (currAdapter == null) { + GroupsAdapter(activity as SimpleActivity, storedGroups, activity, fragment_list, fragment_fastscroller) { + Intent(activity, GroupContactsActivity::class.java).apply { + putExtra(GROUP, it as Group) + activity!!.startActivity(this) + } + }.apply { + addVerticalDividers(true) + fragment_list.adapter = this + } + + fragment_fastscroller.setScrollToY(0) + fragment_fastscroller.setViews(fragment_list) { + val item = (fragment_list.adapter as GroupsAdapter).groups.getOrNull(it) + fragment_fastscroller.updateBubbleText(item?.getBubbleText() ?: "") + } + } else { + (currAdapter as GroupsAdapter).apply { + showContactThumbnails = activity.config.showContactThumbnails + updateItems(storedGroups) + } } } } @@ -251,9 +251,6 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) it.websites.any { it.contains(text, true) } } as ArrayList - Contact.sorting = config.sorting - Contact.startWithSurname = config.startNameWithSurname - filtered.sort() filtered.sortBy { !getProperText(it.getNameToDisplay(), shouldNormalize).startsWith(text, true) } if (filtered.isEmpty() && this@MyViewPagerFragment is FavoritesFragment) { diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/fragments/RecentsFragment.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/fragments/RecentsFragment.kt index 12678e9f..a227c7e9 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/fragments/RecentsFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/fragments/RecentsFragment.kt @@ -1,16 +1,21 @@ package com.simplemobiletools.contacts.pro.fragments +import android.annotation.TargetApi import android.content.Context import android.content.Intent +import android.os.Build +import android.telecom.TelecomManager import android.util.AttributeSet import com.simplemobiletools.commons.extensions.beVisibleIf import com.simplemobiletools.commons.extensions.hasPermission import com.simplemobiletools.commons.helpers.PERMISSION_READ_CALL_LOG import com.simplemobiletools.commons.helpers.PERMISSION_WRITE_CALL_LOG -import com.simplemobiletools.contacts.pro.activities.EditContactActivity +import com.simplemobiletools.commons.helpers.isMarshmallowPlus +import com.simplemobiletools.contacts.pro.activities.InsertOrEditContactActivity import com.simplemobiletools.contacts.pro.adapters.RecentCallsAdapter -import com.simplemobiletools.contacts.pro.extensions.applyRegexFiltering import com.simplemobiletools.contacts.pro.extensions.contactClicked +import com.simplemobiletools.contacts.pro.extensions.isDefaultDialer +import com.simplemobiletools.contacts.pro.extensions.normalizeNumber import com.simplemobiletools.contacts.pro.helpers.IS_FROM_SIMPLE_CONTACTS import com.simplemobiletools.contacts.pro.helpers.KEY_PHONE import com.simplemobiletools.contacts.pro.helpers.RECENTS_TAB_MASK @@ -21,13 +26,19 @@ import kotlinx.android.synthetic.main.fragment_layout.view.* class RecentsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerFragment(context, attributeSet) { override fun fabClicked() {} + @TargetApi(Build.VERSION_CODES.M) override fun placeholderClicked() { - activity!!.handlePermission(PERMISSION_WRITE_CALL_LOG) { - if (it) { - activity!!.handlePermission(PERMISSION_READ_CALL_LOG) { - activity?.refreshContacts(RECENTS_TAB_MASK) + if (!isMarshmallowPlus() || (isMarshmallowPlus() && context.isDefaultDialer())) { + activity!!.handlePermission(PERMISSION_WRITE_CALL_LOG) { + if (it) { + activity!!.handlePermission(PERMISSION_READ_CALL_LOG) { + activity?.refreshContacts(RECENTS_TAB_MASK) + } } } + } else { + val intent = Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER).putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME, context.packageName) + context.startActivity(intent) } } @@ -43,10 +54,10 @@ class RecentsFragment(context: Context, attributeSet: AttributeSet) : MyViewPage val currAdapter = fragment_list.adapter if (currAdapter == null) { RecentCallsAdapter(activity!!, recentCalls, activity, fragment_list, fragment_fastscroller) { - val recentCall = (it as RecentCall).number.applyRegexFiltering() + val recentCall = (it as RecentCall).number.normalizeNumber() var selectedContact: Contact? = null for (contact in allContacts) { - if (contact.phoneNumbers.any { it.value.applyRegexFiltering() == recentCall }) { + if (contact.doesContainPhoneNumber(recentCall)) { selectedContact = contact break } @@ -55,8 +66,8 @@ class RecentsFragment(context: Context, attributeSet: AttributeSet) : MyViewPage if (selectedContact != null) { activity?.contactClicked(selectedContact) } else { - Intent(context, EditContactActivity::class.java).apply { - action = Intent.ACTION_INSERT + Intent(context, InsertOrEditContactActivity::class.java).apply { + action = Intent.ACTION_INSERT_OR_EDIT putExtra(KEY_PHONE, recentCall) putExtra(IS_FROM_SIMPLE_CONTACTS, true) context.startActivity(this) diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/Config.kt index bd1fbcd3..483ac685 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/Config.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/Config.kt @@ -60,4 +60,8 @@ class Config(context: Context) : BaseConfig(context) { var showCallConfirmation: Boolean get() = prefs.getBoolean(SHOW_CALL_CONFIRMATION, false) set(showCallConfirmation) = prefs.edit().putBoolean(SHOW_CALL_CONFIRMATION, showCallConfirmation).apply() + + var showDialpadButton: Boolean + get() = prefs.getBoolean(SHOW_DIALPAD_BUTTON, true) + set(showDialpadButton) = prefs.edit().putBoolean(SHOW_DIALPAD_BUTTON, showDialpadButton).apply() } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/Constants.kt index 9073d347..8826a543 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/Constants.kt @@ -1,6 +1,7 @@ package com.simplemobiletools.contacts.pro.helpers import android.provider.ContactsContract.CommonDataKinds +import com.simplemobiletools.contacts.pro.models.LocalContact // shared prefs const val SHOW_CONTACT_THUMBNAILS = "show_contact_thumbnails" @@ -16,15 +17,17 @@ const val SHOW_CONTACT_FIELDS = "show_contact_fields" const val SHOW_TABS = "show_tabs" const val FILTER_DUPLICATES = "filter_duplicates" const val SHOW_CALL_CONFIRMATION = "show_call_confirmation" +const val SHOW_DIALPAD_BUTTON = "show_dialpad_button" const val CONTACT_ID = "contact_id" const val SMT_PRIVATE = "smt_private" // used at the contact source of local contacts hidden from other apps const val IS_PRIVATE = "is_private" const val GROUP = "group" -const val FIRST_GROUP_ID = 10000 -const val PHONE_NUMBER_PATTERN = "[^0-9#*+]" const val IS_FROM_SIMPLE_CONTACTS = "is_from_simple_contacts" const val ADD_NEW_CONTACT_NUMBER = "add_new_contact_number" +const val FIRST_CONTACT_ID = 1000000 +const val FIRST_GROUP_ID = 10000L +const val REQUEST_CODE_SET_DEFAULT_DIALER = 1 // extras used at third party intents const val KEY_PHONE = "phone" @@ -119,3 +122,5 @@ val localAccountTypes = arrayListOf("vnd.sec.contact.phone", const val TELEGRAM_PACKAGE = "org.telegram.messenger" const val SIGNAL_PACKAGE = "org.thoughtcrime.securesms" const val WHATSAPP_PACKAGE = "com.whatsapp" + +fun getEmptyLocalContact() = LocalContact(0, "", "", "", "", "", "", null, ArrayList(), ArrayList(), ArrayList(), 0, ArrayList(), "", ArrayList(), "", "", ArrayList(), ArrayList()) diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/ContactsHelper.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/ContactsHelper.kt index 57447ffc..8a14f930 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/ContactsHelper.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/ContactsHelper.kt @@ -3,11 +3,12 @@ package com.simplemobiletools.contacts.pro.helpers import android.accounts.Account import android.accounts.AccountManager import android.annotation.SuppressLint -import android.app.Activity import android.content.* import android.database.Cursor import android.graphics.Bitmap import android.net.Uri +import android.os.Handler +import android.os.Looper import android.provider.CallLog import android.provider.ContactsContract import android.provider.ContactsContract.CommonDataKinds @@ -26,24 +27,24 @@ import java.text.SimpleDateFormat import java.util.* import kotlin.collections.ArrayList -class ContactsHelper(val activity: Activity) { +class ContactsHelper(val context: Context) { private val BATCH_SIZE = 100 private var displayContactSources = ArrayList() fun getContacts(callback: (ArrayList) -> Unit) { Thread { val contacts = SparseArray() - displayContactSources = activity.getVisibleContactSources() + displayContactSources = context.getVisibleContactSources() getDeviceContacts(contacts) if (displayContactSources.contains(SMT_PRIVATE)) { - activity.dbHelper.getContacts(activity).forEach { + LocalContactsHelper(context).getAllContacts().forEach { contacts.put(it.id, it) } } val contactsSize = contacts.size() - val showOnlyContactsWithNumbers = activity.config.showOnlyContactsWithNumbers + val showOnlyContactsWithNumbers = context.config.showOnlyContactsWithNumbers var tempContacts = ArrayList(contactsSize) val resultContacts = ArrayList(contactsSize) @@ -57,7 +58,7 @@ class ContactsHelper(val activity: Activity) { contacts.valueAt(it) } - if (activity.config.filterDuplicates) { + if (context.config.filterDuplicates) { tempContacts = tempContacts.distinctBy { it.getHashToCompare() } as ArrayList @@ -75,14 +76,18 @@ class ContactsHelper(val activity: Activity) { } // groups are obtained with contactID, not rawID, so assign them to proper contacts like this - val groups = getContactGroups(getStoredGroups()) + val groups = getContactGroups(getStoredGroupsSync()) val size = groups.size() for (i in 0 until size) { val key = groups.keyAt(i) resultContacts.firstOrNull { it.contactId == key }?.groups = groups.valueAt(i) } - activity.runOnUiThread { + Contact.sorting = context.config.sorting + Contact.startWithSurname = context.config.startNameWithSurname + resultContacts.sort() + + Handler(Looper.getMainLooper()).post { callback(resultContacts) } }.start() @@ -98,7 +103,7 @@ class ContactsHelper(val activity: Activity) { val sources = HashSet() var cursor: Cursor? = null try { - cursor = activity.contentResolver.query(uri, projection, null, null, null) + cursor = context.contentResolver.query(uri, projection, null, null, null) if (cursor?.moveToFirst() == true) { do { val name = cursor.getStringValue(ContactsContract.RawContacts.ACCOUNT_NAME) ?: "" @@ -116,7 +121,7 @@ class ContactsHelper(val activity: Activity) { } private fun getDeviceContacts(contacts: SparseArray) { - if (!activity.hasContactPermissions()) { + if (!context.hasContactPermissions()) { return } @@ -128,7 +133,7 @@ class ContactsHelper(val activity: Activity) { var cursor: Cursor? = null try { - cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, sortOrder) + cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, sortOrder) if (cursor?.moveToFirst() == true) { do { val id = cursor.getIntValue(ContactsContract.Data.RAW_CONTACT_ID) @@ -151,21 +156,19 @@ class ContactsHelper(val activity: Activity) { val groups = ArrayList() val organization = Organization("", "") val websites = ArrayList() - val cleanNumbers = ArrayList() val ims = ArrayList() val contact = Contact(id, prefix, firstName, middleName, surname, suffix, nickname, photoUri, numbers, emails, addresses, - events, accountName, starred, contactId, thumbnailUri, null, notes, groups, organization, websites, cleanNumbers, ims) + events, accountName, starred, contactId, thumbnailUri, null, notes, groups, organization, websites, ims) contacts.put(id, contact) } while (cursor.moveToNext()) } } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) } finally { cursor?.close() } - val filterDuplicates = activity.config.filterDuplicates val phoneNumbers = getPhoneNumbers(null) var size = phoneNumbers.size() for (i in 0 until size) { @@ -173,13 +176,6 @@ class ContactsHelper(val activity: Activity) { if (contacts[key] != null) { val numbers = phoneNumbers.valueAt(i) contacts[key].phoneNumbers = numbers - - if (filterDuplicates) { - // remove all spaces, dashes etc from numbers for easier comparing, used only at list views - numbers.forEach { - numbers.mapTo(contacts[key].cleanPhoneNumbers) { PhoneNumber(it.value.applyRegexFiltering(), 0, "") } - } - } } } @@ -246,6 +242,7 @@ class ContactsHelper(val activity: Activity) { val projection = arrayOf( ContactsContract.Data.RAW_CONTACT_ID, CommonDataKinds.Phone.NUMBER, + CommonDataKinds.Phone.NORMALIZED_NUMBER, CommonDataKinds.Phone.TYPE, CommonDataKinds.Phone.LABEL ) @@ -255,11 +252,12 @@ class ContactsHelper(val activity: Activity) { var cursor: Cursor? = null try { - cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, null) + cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) if (cursor?.moveToFirst() == true) { do { val id = cursor.getIntValue(ContactsContract.Data.RAW_CONTACT_ID) val number = cursor.getStringValue(CommonDataKinds.Phone.NUMBER) ?: continue + val normalizedNumber = cursor.getStringValue(CommonDataKinds.Phone.NORMALIZED_NUMBER) ?: number.normalizeNumber() val type = cursor.getIntValue(CommonDataKinds.Phone.TYPE) val label = cursor.getStringValue(CommonDataKinds.Phone.LABEL) ?: "" @@ -267,12 +265,12 @@ class ContactsHelper(val activity: Activity) { phoneNumbers.put(id, ArrayList()) } - val phoneNumber = PhoneNumber(number, type, label) + val phoneNumber = PhoneNumber(number, type, label, normalizedNumber) phoneNumbers[id].add(phoneNumber) } while (cursor.moveToNext()) } } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) } finally { cursor?.close() } @@ -293,7 +291,7 @@ class ContactsHelper(val activity: Activity) { var cursor: Cursor? = null try { - cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, null) + cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) if (cursor?.moveToFirst() == true) { do { val id = cursor.getIntValue(ContactsContract.Data.RAW_CONTACT_ID) @@ -302,7 +300,7 @@ class ContactsHelper(val activity: Activity) { } while (cursor.moveToNext()) } } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) } finally { cursor?.close() } @@ -325,7 +323,7 @@ class ContactsHelper(val activity: Activity) { var cursor: Cursor? = null try { - cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, null) + cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) if (cursor?.moveToFirst() == true) { do { val id = cursor.getIntValue(ContactsContract.Data.RAW_CONTACT_ID) @@ -341,7 +339,7 @@ class ContactsHelper(val activity: Activity) { } while (cursor.moveToNext()) } } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) } finally { cursor?.close() } @@ -364,7 +362,7 @@ class ContactsHelper(val activity: Activity) { var cursor: Cursor? = null try { - cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, null) + cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) if (cursor?.moveToFirst() == true) { do { val id = cursor.getIntValue(ContactsContract.Data.RAW_CONTACT_ID) @@ -380,7 +378,7 @@ class ContactsHelper(val activity: Activity) { } while (cursor.moveToNext()) } } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) } finally { cursor?.close() } @@ -403,7 +401,7 @@ class ContactsHelper(val activity: Activity) { var cursor: Cursor? = null try { - cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, null) + cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) if (cursor?.moveToFirst() == true) { do { val id = cursor.getIntValue(ContactsContract.Data.RAW_CONTACT_ID) @@ -419,7 +417,7 @@ class ContactsHelper(val activity: Activity) { } while (cursor.moveToNext()) } } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) } finally { cursor?.close() } @@ -441,7 +439,7 @@ class ContactsHelper(val activity: Activity) { var cursor: Cursor? = null try { - cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, null) + cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) if (cursor?.moveToFirst() == true) { do { val id = cursor.getIntValue(ContactsContract.Data.RAW_CONTACT_ID) @@ -456,7 +454,7 @@ class ContactsHelper(val activity: Activity) { } while (cursor.moveToNext()) } } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) } finally { cursor?.close() } @@ -477,7 +475,7 @@ class ContactsHelper(val activity: Activity) { var cursor: Cursor? = null try { - cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, null) + cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) if (cursor?.moveToFirst() == true) { do { val id = cursor.getIntValue(ContactsContract.Data.RAW_CONTACT_ID) @@ -486,7 +484,7 @@ class ContactsHelper(val activity: Activity) { } while (cursor.moveToNext()) } } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) } finally { cursor?.close() } @@ -508,7 +506,7 @@ class ContactsHelper(val activity: Activity) { var cursor: Cursor? = null try { - cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, null) + cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) if (cursor?.moveToFirst() == true) { do { val id = cursor.getIntValue(ContactsContract.Data.RAW_CONTACT_ID) @@ -523,7 +521,7 @@ class ContactsHelper(val activity: Activity) { } while (cursor.moveToNext()) } } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) } finally { cursor?.close() } @@ -544,7 +542,7 @@ class ContactsHelper(val activity: Activity) { var cursor: Cursor? = null try { - cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, null) + cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) if (cursor?.moveToFirst() == true) { do { val id = cursor.getIntValue(ContactsContract.Data.RAW_CONTACT_ID) @@ -558,7 +556,7 @@ class ContactsHelper(val activity: Activity) { } while (cursor.moveToNext()) } } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) } finally { cursor?.close() } @@ -568,7 +566,7 @@ class ContactsHelper(val activity: Activity) { private fun getContactGroups(storedGroups: ArrayList, contactId: Int? = null): SparseArray> { val groups = SparseArray>() - if (!activity.hasContactPermissions()) { + if (!context.hasContactPermissions()) { return groups } @@ -583,7 +581,7 @@ class ContactsHelper(val activity: Activity) { var cursor: Cursor? = null try { - cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, null) + cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) if (cursor?.moveToFirst() == true) { do { val id = cursor.getIntValue(ContactsContract.Data.CONTACT_ID) @@ -598,7 +596,7 @@ class ContactsHelper(val activity: Activity) { } while (cursor.moveToNext()) } } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) } finally { cursor?.close() } @@ -648,15 +646,24 @@ class ContactsHelper(val activity: Activity) { return args.toTypedArray() } - fun getStoredGroups(): ArrayList { + fun getStoredGroups(callback: (ArrayList) -> Unit) { + Thread { + val groups = getStoredGroupsSync() + Handler(Looper.getMainLooper()).post { + callback(groups) + } + }.start() + } + + fun getStoredGroupsSync(): ArrayList { val groups = getDeviceStoredGroups() - groups.addAll(activity.dbHelper.getGroups()) + groups.addAll(context.groupsDB.getGroups()) return groups } fun getDeviceStoredGroups(): ArrayList { val groups = ArrayList() - if (!activity.hasContactPermissions()) { + if (!context.hasContactPermissions()) { return groups } @@ -672,7 +679,7 @@ class ContactsHelper(val activity: Activity) { var cursor: Cursor? = null try { - cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, null) + cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) if (cursor?.moveToFirst() == true) { do { val id = cursor.getLongValue(ContactsContract.Groups._ID) @@ -687,7 +694,7 @@ class ContactsHelper(val activity: Activity) { } while (cursor.moveToNext()) } } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) } finally { cursor?.close() } @@ -696,7 +703,10 @@ class ContactsHelper(val activity: Activity) { fun createNewGroup(title: String, accountName: String, accountType: String): Group? { if (accountType == SMT_PRIVATE) { - return activity.dbHelper.insertGroup(Group(0, title)) + val newGroup = Group(null, title) + val id = context.groupsDB.insertOrUpdate(newGroup) + newGroup.id = id + return newGroup } val operations = ArrayList() @@ -709,11 +719,11 @@ class ContactsHelper(val activity: Activity) { } try { - val results = activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + val results = context.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) val rawId = ContentUris.parseId(results[0].uri) return Group(rawId, title) } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) } return null } @@ -729,9 +739,9 @@ class ContactsHelper(val activity: Activity) { } try { - activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + context.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) } } @@ -744,9 +754,9 @@ class ContactsHelper(val activity: Activity) { operations.add(ContentProviderOperation.newDelete(uri).build()) try { - activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + context.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) } } @@ -754,7 +764,7 @@ class ContactsHelper(val activity: Activity) { if (id == 0) { return null } else if (isLocalPrivate) { - return activity.dbHelper.getContactWithId(activity, id) + return LocalContactsHelper(context).getContactWithId(id) } val selection = "${ContactsContract.Data.MIMETYPE} = ? AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?" @@ -769,12 +779,12 @@ class ContactsHelper(val activity: Activity) { } private fun parseContactCursor(selection: String, selectionArgs: Array): Contact? { - val storedGroups = getStoredGroups() + val storedGroups = getStoredGroupsSync() val uri = ContactsContract.Data.CONTENT_URI val projection = getContactProjection() var cursor: Cursor? = null try { - cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, null) + cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) if (cursor?.moveToFirst() == true) { val id = cursor.getIntValue(ContactsContract.Data.RAW_CONTACT_ID) val prefix = cursor.getStringValue(CommonDataKinds.StructuredName.PREFIX) ?: "" @@ -796,10 +806,9 @@ class ContactsHelper(val activity: Activity) { val thumbnailUri = cursor.getStringValue(CommonDataKinds.StructuredName.PHOTO_THUMBNAIL_URI) ?: "" val organization = getOrganizations(id)[id] ?: Organization("", "") val websites = getWebsites(id)[id] ?: ArrayList() - val cleanNumbers = ArrayList() val ims = getIMs(id)[id] ?: ArrayList() return Contact(id, prefix, firstName, middleName, surname, suffix, nickname, photoUri, number, emails, addresses, events, - accountName, starred, contactId, thumbnailUri, null, notes, groups, organization, websites, cleanNumbers, ims) + accountName, starred, contactId, thumbnailUri, null, notes, groups, organization, websites, ims) } } finally { cursor?.close() @@ -816,22 +825,22 @@ class ContactsHelper(val activity: Activity) { private fun getContactSourcesSync(): ArrayList { val sources = getDeviceContactSources() - sources.add(ContactSource(activity.getString(R.string.phone_storage_hidden), SMT_PRIVATE)) + sources.add(ContactSource(context.getString(R.string.phone_storage_hidden), SMT_PRIVATE)) return ArrayList(sources) } fun getDeviceContactSources(): LinkedHashSet { val sources = LinkedHashSet() - if (!activity.hasContactPermissions()) { + if (!context.hasContactPermissions()) { return sources } - val accounts = AccountManager.get(activity).accounts + val accounts = AccountManager.get(context).accounts accounts.forEach { if (ContentResolver.getIsSyncable(it, ContactsContract.AUTHORITY) == 1) { val contactSource = ContactSource(it.name, it.type) if (it.type == TELEGRAM_PACKAGE) { - contactSource.name += " (${activity.getString(R.string.telegram)})" + contactSource.name += " (${context.getString(R.string.telegram)})" } sources.add(contactSource) } @@ -842,7 +851,7 @@ class ContactsHelper(val activity: Activity) { } sources.addAll(contentResolverAccounts) - if (sources.isEmpty() && activity.config.localAccountName.isEmpty() && activity.config.localAccountType.isEmpty()) { + if (sources.isEmpty() && context.config.localAccountName.isEmpty() && context.config.localAccountType.isEmpty()) { sources.add(ContactSource("", "")) } @@ -866,7 +875,7 @@ class ContactsHelper(val activity: Activity) { ) private fun getSortString(): String { - val sorting = activity.config.sorting + val sorting = context.config.sorting var sort = when { sorting and SORT_BY_FIRST_NAME != 0 -> "${CommonDataKinds.StructuredName.GIVEN_NAME} COLLATE NOCASE" sorting and SORT_BY_MIDDLE_NAME != 0 -> "${CommonDataKinds.StructuredName.MIDDLE_NAME} COLLATE NOCASE" @@ -888,7 +897,7 @@ class ContactsHelper(val activity: Activity) { val selectionArgs = arrayOf(CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, id.toString()) var cursor: Cursor? = null try { - cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, null) + cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) if (cursor?.moveToFirst() == true) { return cursor.getIntValue(ContactsContract.Data.CONTACT_ID) } @@ -900,9 +909,9 @@ class ContactsHelper(val activity: Activity) { } fun updateContact(contact: Contact, photoUpdateStatus: Int): Boolean { - activity.toast(R.string.updating) - if (contact.source == SMT_PRIVATE) { - return activity.dbHelper.updateContact(contact) + context.toast(R.string.updating) + if (contact.isPrivate()) { + return LocalContactsHelper(context).insertOrUpdateContact(contact) } try { @@ -949,6 +958,7 @@ class ContactsHelper(val activity: Activity) { withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.id) withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) withValue(CommonDataKinds.Phone.NUMBER, it.value) + withValue(CommonDataKinds.Phone.NORMALIZED_NUMBER, it.normalizedNumber) withValue(CommonDataKinds.Phone.TYPE, it.type) withValue(CommonDataKinds.Phone.LABEL, it.label) operations.add(build()) @@ -1089,7 +1099,7 @@ class ContactsHelper(val activity: Activity) { } // delete groups - val relevantGroupIDs = getStoredGroups().map { it.id } + val relevantGroupIDs = getStoredGroupsSync().map { it.id } if (relevantGroupIDs.isNotEmpty()) { val IDsString = TextUtils.join(",", relevantGroupIDs) ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI).apply { @@ -1115,9 +1125,9 @@ class ContactsHelper(val activity: Activity) { val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, contact.contactId.toString()) val contentValues = ContentValues(1) contentValues.put(ContactsContract.Contacts.STARRED, contact.starred) - activity.contentResolver.update(uri, contentValues, null, null) + context.contentResolver.update(uri, contentValues, null, null) } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) } // photo @@ -1126,10 +1136,10 @@ class ContactsHelper(val activity: Activity) { PHOTO_REMOVED -> removePhoto(contact, operations) } - activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + context.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) return true } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) return false } } @@ -1137,9 +1147,9 @@ class ContactsHelper(val activity: Activity) { private fun addPhoto(contact: Contact, operations: ArrayList): ArrayList { if (contact.photoUri.isNotEmpty()) { val photoUri = Uri.parse(contact.photoUri) - val bitmap = MediaStore.Images.Media.getBitmap(activity.contentResolver, photoUri) + val bitmap = MediaStore.Images.Media.getBitmap(context.contentResolver, photoUri) - val thumbnailSize = activity.getPhotoThumbnailSize() + val thumbnailSize = context.getPhotoThumbnailSize() val scaledPhoto = Bitmap.createScaledBitmap(bitmap, thumbnailSize, thumbnailSize, false) val scaledSizePhotoData = scaledPhoto.getByteArray() scaledPhoto.recycle() @@ -1181,15 +1191,15 @@ class ContactsHelper(val activity: Activity) { } if (operations.size % BATCH_SIZE == 0) { - activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + context.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) operations.clear() } } try { - activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + context.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) } } @@ -1204,16 +1214,16 @@ class ContactsHelper(val activity: Activity) { } if (operations.size % BATCH_SIZE == 0) { - activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + context.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) operations.clear() } } - activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + context.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) } fun insertContact(contact: Contact): Boolean { - if (contact.source == SMT_PRIVATE) { - return insertLocalContact(contact) + if (contact.isPrivate()) { + return LocalContactsHelper(context).insertOrUpdateContact(contact) } try { @@ -1251,6 +1261,7 @@ class ContactsHelper(val activity: Activity) { withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) withValue(CommonDataKinds.Phone.NUMBER, it.value) + withValue(CommonDataKinds.Phone.NORMALIZED_NUMBER, it.normalizedNumber) withValue(CommonDataKinds.Phone.TYPE, it.type) withValue(CommonDataKinds.Phone.LABEL, it.label) operations.add(build()) @@ -1351,9 +1362,9 @@ class ContactsHelper(val activity: Activity) { var scaledSizePhotoData: ByteArray? if (contact.photoUri.isNotEmpty()) { val photoUri = Uri.parse(contact.photoUri) - val bitmap = MediaStore.Images.Media.getBitmap(activity.contentResolver, photoUri) + val bitmap = MediaStore.Images.Media.getBitmap(context.contentResolver, photoUri) - val thumbnailSize = activity.getPhotoThumbnailSize() + val thumbnailSize = context.getPhotoThumbnailSize() val scaledPhoto = Bitmap.createScaledBitmap(bitmap, thumbnailSize, thumbnailSize, false) scaledSizePhotoData = scaledPhoto.getByteArray() @@ -1371,7 +1382,7 @@ class ContactsHelper(val activity: Activity) { val results: Array try { - results = activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + results = context.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) } finally { scaledSizePhotoData = null } @@ -1388,22 +1399,20 @@ class ContactsHelper(val activity: Activity) { val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, userId.toString()) val contentValues = ContentValues(1) contentValues.put(ContactsContract.Contacts.STARRED, contact.starred) - activity.contentResolver.update(uri, contentValues, null, null) + context.contentResolver.update(uri, contentValues, null, null) } return true } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) return false } } - private fun insertLocalContact(contact: Contact) = activity.dbHelper.insertContact(contact) - private fun addFullSizePhoto(contactId: Long, fullSizePhotoData: ByteArray) { val baseUri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, contactId) val displayPhotoUri = Uri.withAppendedPath(baseUri, ContactsContract.RawContacts.DisplayPhoto.CONTENT_DIRECTORY) - val fileDescriptor = activity.contentResolver.openAssetFileDescriptor(displayPhotoUri, "rw") + val fileDescriptor = context.contentResolver.openAssetFileDescriptor(displayPhotoUri, "rw") val photoStream = fileDescriptor.createOutputStream() photoStream.write(fullSizePhotoData) photoStream.close() @@ -1417,7 +1426,7 @@ class ContactsHelper(val activity: Activity) { val selectionArgs = arrayOf(CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, contactId) var cursor: Cursor? = null try { - cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, null) + cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) if (cursor?.moveToFirst() == true) { val id = cursor.getIntValue(ContactsContract.Data.CONTACT_ID) val lookupKey = cursor.getStringValue(ContactsContract.Data.LOOKUP_KEY) @@ -1437,7 +1446,7 @@ class ContactsHelper(val activity: Activity) { var cursor: Cursor? = null try { - cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, null) + cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) if (cursor?.moveToFirst() == true) { return cursor.getStringValue(ContactsContract.Data._ID) } @@ -1448,23 +1457,27 @@ class ContactsHelper(val activity: Activity) { } fun addFavorites(contacts: ArrayList) { - toggleLocalFavorites(contacts, true) - if (activity.hasContactPermissions()) { - toggleFavorites(contacts, true) - } + Thread { + toggleLocalFavorites(contacts, true) + if (context.hasContactPermissions()) { + toggleFavorites(contacts, true) + } + }.start() } fun removeFavorites(contacts: ArrayList) { - toggleLocalFavorites(contacts, false) - if (activity.hasContactPermissions()) { - toggleFavorites(contacts, false) - } + Thread { + toggleLocalFavorites(contacts, false) + if (context.hasContactPermissions()) { + toggleFavorites(contacts, false) + } + }.start() } private fun toggleFavorites(contacts: ArrayList, addToFavorites: Boolean) { try { val operations = ArrayList() - contacts.filter { it.source != SMT_PRIVATE }.map { it.contactId.toString() }.forEach { + contacts.filter { !it.isPrivate() }.map { it.contactId.toString() }.forEach { val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, it) ContentProviderOperation.newUpdate(uri).apply { withValue(ContactsContract.Contacts.STARRED, if (addToFavorites) 1 else 0) @@ -1472,68 +1485,69 @@ class ContactsHelper(val activity: Activity) { } if (operations.size % BATCH_SIZE == 0) { - activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + context.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) operations.clear() } } - activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + context.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) } } private fun toggleLocalFavorites(contacts: ArrayList, addToFavorites: Boolean) { - val localContacts = contacts.filter { it.source == SMT_PRIVATE }.map { it.id.toString() }.toTypedArray() - activity.dbHelper.toggleFavorites(localContacts, addToFavorites) + val localContacts = contacts.filter { it.isPrivate() }.map { it.id }.toTypedArray() + LocalContactsHelper(context).toggleFavorites(localContacts, addToFavorites) } fun deleteContact(contact: Contact) { - if (contact.source == SMT_PRIVATE) { - activity.dbHelper.deleteContact(contact.id) - } else { - deleteContacts(arrayListOf(contact)) - } + Thread { + if (contact.isPrivate()) { + context.contactsDB.deleteContactId(contact.id) + } else { + deleteContacts(arrayListOf(contact)) + } + }.start() } fun deleteContacts(contacts: ArrayList) { - Thread { - val localContacts = contacts.filter { it.source == SMT_PRIVATE }.map { it.id.toString() }.toTypedArray() - activity.dbHelper.deleteContacts(localContacts) + val localContacts = contacts.filter { it.isPrivate() }.map { it.id }.toTypedArray() + LocalContactsHelper(context).deleteContactIds(localContacts) - try { - val operations = ArrayList() - val selection = "${ContactsContract.RawContacts._ID} = ?" - contacts.filter { it.source != SMT_PRIVATE }.forEach { - ContentProviderOperation.newDelete(ContactsContract.RawContacts.CONTENT_URI).apply { - val selectionArgs = arrayOf(it.id.toString()) - withSelection(selection, selectionArgs) - operations.add(build()) - } - - if (operations.size % BATCH_SIZE == 0) { - activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) - operations.clear() - } + try { + val operations = ArrayList() + val selection = "${ContactsContract.RawContacts._ID} = ?" + contacts.filter { !it.isPrivate() }.forEach { + ContentProviderOperation.newDelete(ContactsContract.RawContacts.CONTENT_URI).apply { + val selectionArgs = arrayOf(it.id.toString()) + withSelection(selection, selectionArgs) + operations.add(build()) } - if (activity.hasPermission(PERMISSION_WRITE_CONTACTS)) { - activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + if (operations.size % BATCH_SIZE == 0) { + context.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + operations.clear() } - } catch (e: Exception) { - activity.showErrorToast(e) } - }.start() + + if (context.hasPermission(PERMISSION_WRITE_CONTACTS)) { + context.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + } + } catch (e: Exception) { + context.showErrorToast(e) + } } @SuppressLint("MissingPermission") fun getRecents(callback: (ArrayList) -> Unit) { Thread { val calls = ArrayList() - if (!activity.hasPermission(PERMISSION_WRITE_CALL_LOG) || !activity.hasPermission(PERMISSION_READ_CALL_LOG)) { + if (!context.hasPermission(PERMISSION_WRITE_CALL_LOG) || !context.hasPermission(PERMISSION_READ_CALL_LOG)) { callback(calls) return@Thread } + val blockedNumbers = context.getBlockedNumbers() val uri = CallLog.Calls.CONTENT_URI val projection = arrayOf( CallLog.Calls._ID, @@ -1547,13 +1561,13 @@ class ContactsHelper(val activity: Activity) { val currentYear = SimpleDateFormat("yyyy", Locale.getDefault()).format(currentDate) val todayDate = SimpleDateFormat("dd MMM yyyy", Locale.getDefault()).format(currentDate) val yesterdayDate = SimpleDateFormat("dd MMM yyyy", Locale.getDefault()).format(Date(System.currentTimeMillis() - DAY_SECONDS * 1000)) - val yesterday = activity.getString(R.string.yesterday) - val timeFormat = if (activity.config.use24HourFormat) "HH:mm" else "h:mm a" + val yesterday = context.getString(R.string.yesterday) + val timeFormat = if (context.config.use24HourFormat) "HH:mm" else "h:mm a" var prevNumber = "" var cursor: Cursor? = null try { - cursor = activity.contentResolver.query(uri, projection, null, null, sorting) + cursor = context.contentResolver.query(uri, projection, null, null, sorting) if (cursor?.moveToFirst() == true) { do { val id = cursor.getIntValue(CallLog.Calls._ID) @@ -1564,6 +1578,10 @@ class ContactsHelper(val activity: Activity) { continue } + if (blockedNumbers.any { it.number == number || it.normalizedNumber == number }) { + continue + } + var formattedDate = SimpleDateFormat("dd MMM yyyy, $timeFormat", Locale.getDefault()).format(Date(date)) val datePart = formattedDate.substring(0, 11) when { @@ -1597,14 +1615,14 @@ class ContactsHelper(val activity: Activity) { } if (operations.size % BATCH_SIZE == 0) { - activity.contentResolver.applyBatch(CallLog.AUTHORITY, operations) + context.contentResolver.applyBatch(CallLog.AUTHORITY, operations) operations.clear() } } - activity.contentResolver.applyBatch(CallLog.AUTHORITY, operations) + context.contentResolver.applyBatch(CallLog.AUTHORITY, operations) } catch (e: Exception) { - activity.showErrorToast(e) + context.showErrorToast(e) } }.start() } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/Converters.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/Converters.kt new file mode 100644 index 00000000..63205ad6 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/Converters.kt @@ -0,0 +1,59 @@ +package com.simplemobiletools.contacts.pro.helpers + +import androidx.room.TypeConverter +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.simplemobiletools.contacts.pro.models.* + +class Converters { + private val gson = Gson() + private val longType = object : TypeToken>() {}.type + private val stringType = object : TypeToken>() {}.type + private val numberType = object : TypeToken>() {}.type + private val emailType = object : TypeToken>() {}.type + private val addressType = object : TypeToken>() {}.type + private val eventType = object : TypeToken>() {}.type + private val imType = object : TypeToken>() {}.type + + @TypeConverter + fun jsonToStringList(value: String) = gson.fromJson>(value, stringType) + + @TypeConverter + fun stringListToJson(list: ArrayList) = gson.toJson(list) + + @TypeConverter + fun jsonToLongList(value: String) = gson.fromJson>(value, longType) + + @TypeConverter + fun longListToJson(list: ArrayList) = gson.toJson(list) + + @TypeConverter + fun jsonToPhoneNumberList(value: String) = gson.fromJson>(value, numberType) + + @TypeConverter + fun phoneNumberListToJson(list: ArrayList) = gson.toJson(list) + + @TypeConverter + fun jsonToEmailList(value: String) = gson.fromJson>(value, emailType) + + @TypeConverter + fun emailListToJson(list: ArrayList) = gson.toJson(list) + + @TypeConverter + fun jsonToAddressList(value: String) = gson.fromJson>(value, addressType) + + @TypeConverter + fun addressListToJson(list: ArrayList
) = gson.toJson(list) + + @TypeConverter + fun jsonToEventList(value: String) = gson.fromJson>(value, eventType) + + @TypeConverter + fun eventListToJson(list: ArrayList) = gson.toJson(list) + + @TypeConverter + fun jsonToIMsList(value: String) = gson.fromJson>(value, imType) + + @TypeConverter + fun IMsListToJson(list: ArrayList) = gson.toJson(list) +} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/DBHelper.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/DBHelper.kt deleted file mode 100644 index 645c1a61..00000000 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/DBHelper.kt +++ /dev/null @@ -1,369 +0,0 @@ -package com.simplemobiletools.contacts.pro.helpers - -import android.app.Activity -import android.content.ContentValues -import android.content.Context -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.net.Uri -import android.provider.MediaStore -import android.text.TextUtils -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import com.simplemobiletools.commons.extensions.getBlobValue -import com.simplemobiletools.commons.extensions.getIntValue -import com.simplemobiletools.commons.extensions.getLongValue -import com.simplemobiletools.commons.extensions.getStringValue -import com.simplemobiletools.contacts.pro.extensions.applyRegexFiltering -import com.simplemobiletools.contacts.pro.extensions.config -import com.simplemobiletools.contacts.pro.extensions.getByteArray -import com.simplemobiletools.contacts.pro.extensions.getPhotoThumbnailSize -import com.simplemobiletools.contacts.pro.models.* - -class DBHelper private constructor(val context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) { - private val CONTACTS_TABLE_NAME = "contacts" - private val COL_ID = "id" - private val COL_PREFIX = "prefix" - private val COL_FIRST_NAME = "first_name" - private val COL_MIDDLE_NAME = "middle_name" - private val COL_SURNAME = "surname" - private val COL_SUFFIX = "suffix" - private val COL_NICKNAME = "nickname" - private val COL_PHOTO = "photo" - private val COL_PHONE_NUMBERS = "phone_numbers" - private val COL_EMAILS = "emails" - private val COL_EVENTS = "events" - private val COL_STARRED = "starred" - private val COL_ADDRESSES = "addresses" - private val COL_IMS = "ims" - private val COL_NOTES = "notes" - private val COL_COMPANY = "company" - private val COL_JOB_POSITION = "job_position" - private val COL_GROUPS = "groups" - private val COL_WEBSITES = "websites" - - private val GROUPS_TABLE_NAME = "groups" - private val COL_TITLE = "title" - - private val FIRST_CONTACT_ID = 1000000 - - private val mDb = writableDatabase - - companion object { - const val DB_NAME = "contacts.db" - private const val DB_VERSION = 7 - private var dbInstance: DBHelper? = null - var gson = Gson() - - fun newInstance(context: Context): DBHelper { - if (dbInstance == null) - dbInstance = DBHelper(context) - - return dbInstance!! - } - } - - override fun onCreate(db: SQLiteDatabase) { - db.execSQL("CREATE TABLE $CONTACTS_TABLE_NAME ($COL_ID INTEGER PRIMARY KEY AUTOINCREMENT, $COL_FIRST_NAME TEXT, $COL_MIDDLE_NAME TEXT, " + - "$COL_SURNAME TEXT, $COL_PHOTO BLOB, $COL_PHONE_NUMBERS TEXT, $COL_EMAILS TEXT, $COL_EVENTS TEXT, $COL_STARRED INTEGER, " + - "$COL_ADDRESSES TEXT, $COL_NOTES TEXT, $COL_GROUPS TEXT, $COL_PREFIX TEXT, $COL_SUFFIX TEXT, $COL_COMPANY TEXT, $COL_JOB_POSITION TEXT," + - "$COL_WEBSITES TEXT, $COL_NICKNAME TEXT, $COL_IMS TEXT)") - - // start autoincrement ID from FIRST_CONTACT_ID to avoid conflicts - db.execSQL("REPLACE INTO sqlite_sequence (name, seq) VALUES ('$CONTACTS_TABLE_NAME', $FIRST_CONTACT_ID)") - - createGroupsTable(db) - } - - override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - if (oldVersion == 1) { - db.execSQL("ALTER TABLE $CONTACTS_TABLE_NAME ADD COLUMN $COL_ADDRESSES TEXT DEFAULT ''") - db.execSQL("ALTER TABLE $CONTACTS_TABLE_NAME ADD COLUMN $COL_NOTES TEXT DEFAULT ''") - } - - if (oldVersion < 3) { - createGroupsTable(db) - db.execSQL("ALTER TABLE $CONTACTS_TABLE_NAME ADD COLUMN $COL_GROUPS TEXT DEFAULT ''") - } - - if (oldVersion < 4) { - db.execSQL("ALTER TABLE $CONTACTS_TABLE_NAME ADD COLUMN $COL_PREFIX TEXT DEFAULT ''") - db.execSQL("ALTER TABLE $CONTACTS_TABLE_NAME ADD COLUMN $COL_SUFFIX TEXT DEFAULT ''") - db.execSQL("ALTER TABLE $CONTACTS_TABLE_NAME ADD COLUMN $COL_COMPANY TEXT DEFAULT ''") - db.execSQL("ALTER TABLE $CONTACTS_TABLE_NAME ADD COLUMN $COL_JOB_POSITION TEXT DEFAULT ''") - } - - if (oldVersion < 5) { - db.execSQL("ALTER TABLE $CONTACTS_TABLE_NAME ADD COLUMN $COL_WEBSITES TEXT DEFAULT ''") - } - - if (oldVersion < 6) { - db.execSQL("ALTER TABLE $CONTACTS_TABLE_NAME ADD COLUMN $COL_NICKNAME TEXT DEFAULT ''") - } - - if (oldVersion < 7) { - db.execSQL("ALTER TABLE $CONTACTS_TABLE_NAME ADD COLUMN $COL_IMS TEXT DEFAULT ''") - } - } - - private fun createGroupsTable(db: SQLiteDatabase) { - db.execSQL("CREATE TABLE $GROUPS_TABLE_NAME ($COL_ID INTEGER PRIMARY KEY AUTOINCREMENT, $COL_TITLE TEXT)") - - // start autoincrement ID from FIRST_GROUP_ID to avoid conflicts - db.execSQL("REPLACE INTO sqlite_sequence (name, seq) VALUES ('$GROUPS_TABLE_NAME', $FIRST_GROUP_ID)") - } - - fun insertContact(contact: Contact): Boolean { - val contactValues = fillContactValues(contact) - val id = mDb.insert(CONTACTS_TABLE_NAME, null, contactValues).toInt() - return id != -1 - } - - fun updateContact(contact: Contact): Boolean { - val contactValues = fillContactValues(contact) - val selection = "$COL_ID = ?" - val selectionArgs = arrayOf(contact.id.toString()) - return mDb.update(CONTACTS_TABLE_NAME, contactValues, selection, selectionArgs) == 1 - } - - fun deleteContact(id: Int) = deleteContacts(arrayOf(id.toString())) - - fun deleteContacts(ids: Array) { - if (ids.isEmpty()) { - return - } - - val args = TextUtils.join(", ", ids) - val selection = "$CONTACTS_TABLE_NAME.$COL_ID IN ($args)" - mDb.delete(CONTACTS_TABLE_NAME, selection, null) - } - - private fun fillContactValues(contact: Contact): ContentValues { - return ContentValues().apply { - put(COL_PREFIX, contact.prefix) - put(COL_FIRST_NAME, contact.firstName) - put(COL_MIDDLE_NAME, contact.middleName) - put(COL_SURNAME, contact.surname) - put(COL_SUFFIX, contact.suffix) - put(COL_NICKNAME, contact.nickname) - put(COL_PHONE_NUMBERS, gson.toJson(contact.phoneNumbers)) - put(COL_EMAILS, gson.toJson(contact.emails)) - put(COL_ADDRESSES, gson.toJson(contact.addresses)) - put(COL_IMS, gson.toJson(contact.IMs)) - put(COL_EVENTS, gson.toJson(contact.events)) - put(COL_STARRED, contact.starred) - put(COL_NOTES, contact.notes) - put(COL_GROUPS, gson.toJson(contact.groups.map { it.id })) - put(COL_COMPANY, contact.organization.company) - put(COL_JOB_POSITION, contact.organization.jobPosition) - put(COL_WEBSITES, gson.toJson(contact.websites)) - - if (contact.photoUri.isNotEmpty()) { - put(COL_PHOTO, getPhotoByteArray(contact.photoUri)) - } else if (contact.photo == null) { - putNull(COL_PHOTO) - } - } - } - - private fun getPhotoByteArray(uri: String): ByteArray { - val photoUri = Uri.parse(uri) - val bitmap = MediaStore.Images.Media.getBitmap(context.contentResolver, photoUri) - - val thumbnailSize = context.getPhotoThumbnailSize() - val scaledPhoto = Bitmap.createScaledBitmap(bitmap, thumbnailSize * 2, thumbnailSize * 2, false) - val scaledSizePhotoData = scaledPhoto.getByteArray() - scaledPhoto.recycle() - return scaledSizePhotoData - } - - fun toggleFavorites(ids: Array, addToFavorites: Boolean) { - val contactValues = ContentValues() - contactValues.put(COL_STARRED, if (addToFavorites) 1 else 0) - - val args = TextUtils.join(", ", ids) - val selection = "$COL_ID IN ($args)" - mDb.update(CONTACTS_TABLE_NAME, contactValues, selection, null) - } - - fun insertGroup(group: Group): Group? { - val contactValues = fillGroupValues(group) - val id = mDb.insert(GROUPS_TABLE_NAME, null, contactValues) - return if (id == -1L) { - null - } else { - Group(id, group.title) - } - } - - fun renameGroup(group: Group): Boolean { - val contactValues = fillGroupValues(group) - val selection = "$COL_ID = ?" - val selectionArgs = arrayOf(group.id.toString()) - return mDb.update(GROUPS_TABLE_NAME, contactValues, selection, selectionArgs) == 1 - } - - fun deleteGroup(id: Long) = deleteGroups(arrayOf(id.toString())) - - private fun deleteGroups(ids: Array) { - val args = TextUtils.join(", ", ids) - val selection = "$GROUPS_TABLE_NAME.$COL_ID IN ($args)" - mDb.delete(GROUPS_TABLE_NAME, selection, null) - } - - fun getGroups(): ArrayList { - val groups = ArrayList() - val projection = arrayOf(COL_ID, COL_TITLE) - val cursor = mDb.query(GROUPS_TABLE_NAME, projection, null, null, null, null, null) - cursor.use { - while (cursor.moveToNext()) { - val id = cursor.getLongValue(COL_ID) - val title = cursor.getStringValue(COL_TITLE) - val group = Group(id, title) - groups.add(group) - } - } - return groups - } - - private fun fillGroupValues(group: Group): ContentValues { - return ContentValues().apply { - put(COL_TITLE, group.title) - } - } - - fun addContactsToGroup(contacts: ArrayList, groupId: Long) { - contacts.forEach { - val currentGroupIds = it.groups.map { it.id } as ArrayList - currentGroupIds.add(groupId) - updateContactGroups(it, currentGroupIds) - } - } - - fun removeContactsFromGroup(contacts: ArrayList, groupId: Long) { - contacts.forEach { - val currentGroupIds = it.groups.map { it.id } as ArrayList - currentGroupIds.remove(groupId) - updateContactGroups(it, currentGroupIds) - } - } - - private fun updateContactGroups(contact: Contact, groupIds: ArrayList) { - val contactValues = fillContactGroupValues(groupIds) - val selection = "$COL_ID = ?" - val selectionArgs = arrayOf(contact.id.toString()) - mDb.update(CONTACTS_TABLE_NAME, contactValues, selection, selectionArgs) - } - - private fun fillContactGroupValues(groupIds: ArrayList): ContentValues { - return ContentValues().apply { - put(COL_GROUPS, gson.toJson(groupIds)) - } - } - - fun getContacts(activity: Activity, selection: String? = null, selectionArgs: Array? = null): ArrayList { - val storedGroups = ContactsHelper(activity).getStoredGroups() - val filterDuplicates = activity.config.filterDuplicates - val contacts = ArrayList() - val projection = arrayOf(COL_ID, COL_PREFIX, COL_FIRST_NAME, COL_MIDDLE_NAME, COL_SURNAME, COL_SUFFIX, COL_NICKNAME, COL_PHONE_NUMBERS, - COL_EMAILS, COL_EVENTS, COL_STARRED, COL_PHOTO, COL_ADDRESSES, COL_IMS, COL_NOTES, COL_GROUPS, COL_COMPANY, COL_JOB_POSITION, COL_WEBSITES) - - val phoneNumbersToken = object : TypeToken>() {}.type - val emailsToken = object : TypeToken>() {}.type - val addressesToken = object : TypeToken>() {}.type - val IMsToken = object : TypeToken>() {}.type - val eventsToken = object : TypeToken>() {}.type - val groupIdsToken = object : TypeToken>() {}.type - val websitesToken = object : TypeToken>() {}.type - - val cursor = mDb.query(CONTACTS_TABLE_NAME, projection, selection, selectionArgs, null, null, null) - cursor.use { - while (cursor.moveToNext()) { - val id = cursor.getIntValue(COL_ID) - val prefix = cursor.getStringValue(COL_PREFIX) - val firstName = cursor.getStringValue(COL_FIRST_NAME) - val middleName = cursor.getStringValue(COL_MIDDLE_NAME) - val surname = cursor.getStringValue(COL_SURNAME) - val suffix = cursor.getStringValue(COL_SUFFIX) - val nickname = cursor.getStringValue(COL_NICKNAME) - - val phoneNumbersJson = cursor.getStringValue(COL_PHONE_NUMBERS) - val phoneNumbers = if (phoneNumbersJson == "[]") ArrayList() else gson.fromJson>(phoneNumbersJson, phoneNumbersToken) - ?: ArrayList(1) - - // labels can be null at upgrading from older app versions, when the label wasn't available at all yet - phoneNumbers.filter { it.label == null }.forEach { - it.label = "" - } - - val emailsJson = cursor.getStringValue(COL_EMAILS) - val emails = if (emailsJson == "[]") ArrayList() else gson.fromJson>(emailsJson, emailsToken) - ?: ArrayList(1) - - emails.filter { it.label == null }.forEach { - it.label = "" - } - - val addressesJson = cursor.getStringValue(COL_ADDRESSES) - val addresses = if (addressesJson == "[]") ArrayList() else gson.fromJson>(addressesJson, addressesToken) - ?: ArrayList(1) - - addresses.filter { it.label == null }.forEach { - it.label = "" - } - - val IMsJson = cursor.getStringValue(COL_IMS) - val IMs = if (IMsJson == "[]") ArrayList() else gson.fromJson>(IMsJson, IMsToken) ?: ArrayList(1) - - val eventsJson = cursor.getStringValue(COL_EVENTS) - val events = if (eventsJson == "[]") ArrayList() else gson.fromJson>(eventsJson, eventsToken) - ?: ArrayList(1) - - val photoByteArray = cursor.getBlobValue(COL_PHOTO) ?: null - val photo = if (photoByteArray?.isNotEmpty() == true) { - try { - BitmapFactory.decodeByteArray(photoByteArray, 0, photoByteArray.size) - } catch (e: OutOfMemoryError) { - null - } - } else { - null - } - - val notes = cursor.getStringValue(COL_NOTES) - val starred = cursor.getIntValue(COL_STARRED) - - val groupIdsJson = cursor.getStringValue(COL_GROUPS) - val groupIds = if (groupIdsJson == "[]") ArrayList() else gson.fromJson>(groupIdsJson, groupIdsToken) - ?: ArrayList(1) - val groups = storedGroups.filter { groupIds.contains(it.id) } as ArrayList - - val company = cursor.getStringValue(COL_COMPANY) - val jobPosition = cursor.getStringValue(COL_JOB_POSITION) - val organization = Organization(company, jobPosition) - - val websitesJson = cursor.getStringValue(COL_WEBSITES) - val websites = if (websitesJson == "[]") ArrayList() else gson.fromJson>(websitesJson, websitesToken) - ?: ArrayList(1) - - val cleanPhoneNumbers = ArrayList() - if (filterDuplicates) { - phoneNumbers.mapTo(cleanPhoneNumbers) { PhoneNumber(it.value.applyRegexFiltering(), 0, "") } - } - - val contact = Contact(id, prefix, firstName, middleName, surname, suffix, nickname, "", phoneNumbers, emails, addresses, - events, SMT_PRIVATE, starred, id, "", photo, notes, groups, organization, websites, cleanPhoneNumbers, IMs) - contacts.add(contact) - } - } - return contacts - } - - fun getContactWithId(activity: Activity, id: Int): Contact? { - val selection = "$COL_ID = ?" - val selectionArgs = arrayOf(id.toString()) - return getContacts(activity, selection, selectionArgs).firstOrNull() - } -} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/LocalContactsHelper.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/LocalContactsHelper.kt new file mode 100644 index 00000000..54045fa3 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/LocalContactsHelper.kt @@ -0,0 +1,142 @@ +package com.simplemobiletools.contacts.pro.helpers + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.provider.MediaStore +import com.simplemobiletools.contacts.pro.extensions.contactsDB +import com.simplemobiletools.contacts.pro.extensions.getByteArray +import com.simplemobiletools.contacts.pro.extensions.getEmptyContact +import com.simplemobiletools.contacts.pro.extensions.getPhotoThumbnailSize +import com.simplemobiletools.contacts.pro.models.Contact +import com.simplemobiletools.contacts.pro.models.Group +import com.simplemobiletools.contacts.pro.models.LocalContact +import com.simplemobiletools.contacts.pro.models.Organization + +class LocalContactsHelper(val context: Context) { + fun getAllContacts() = context.contactsDB.getContacts().map { convertLocalContactToContact(it) }.toMutableList() as ArrayList + + fun getContactWithId(id: Int) = convertLocalContactToContact(context.contactsDB.getContactWithId(id)) + + fun insertOrUpdateContact(contact: Contact): Boolean { + val localContact = convertContactToLocalContact(contact) + return context.contactsDB.insertOrUpdate(localContact) > 0 + } + + fun addContactsToGroup(contacts: ArrayList, groupId: Long) { + contacts.forEach { + val localContact = convertContactToLocalContact(it) + val newGroups = localContact.groups + newGroups.add(groupId) + newGroups.distinct() + localContact.groups = newGroups + context.contactsDB.insertOrUpdate(localContact) + } + } + + fun removeContactsFromGroup(contacts: ArrayList, groupId: Long) { + contacts.forEach { + val localContact = convertContactToLocalContact(it) + val newGroups = localContact.groups + newGroups.remove(groupId) + localContact.groups = newGroups + context.contactsDB.insertOrUpdate(localContact) + } + } + + fun deleteContactIds(ids: Array) { + ids.forEach { + context.contactsDB.deleteContactId(it) + } + } + + fun toggleFavorites(ids: Array, addToFavorites: Boolean) { + val isStarred = if (addToFavorites) 1 else 0 + ids.forEach { + context.contactsDB.updateStarred(isStarred, it) + } + } + + private fun getPhotoByteArray(uri: String): ByteArray { + if (uri.isEmpty()) { + return ByteArray(0) + } + + val photoUri = Uri.parse(uri) + val bitmap = MediaStore.Images.Media.getBitmap(context.contentResolver, photoUri) + + val thumbnailSize = context.getPhotoThumbnailSize() + val scaledPhoto = Bitmap.createScaledBitmap(bitmap, thumbnailSize * 2, thumbnailSize * 2, false) + val scaledSizePhotoData = scaledPhoto.getByteArray() + scaledPhoto.recycle() + return scaledSizePhotoData + } + + private fun convertLocalContactToContact(localContact: LocalContact?): Contact? { + if (localContact == null) { + return null + } + + val contactPhoto = if (localContact.photo == null) { + null + } else { + try { + BitmapFactory.decodeByteArray(localContact.photo, 0, localContact.photo!!.size) + } catch (e: OutOfMemoryError) { + null + } + } + + val storedGroups = ContactsHelper(context).getStoredGroupsSync() + + return context.getEmptyContact().apply { + id = localContact.id!! + prefix = localContact.prefix + firstName = localContact.firstName + middleName = localContact.middleName + surname = localContact.surname + suffix = localContact.suffix + nickname = localContact.nickname + photoUri = "" + phoneNumbers = localContact.phoneNumbers + emails = localContact.emails + addresses = localContact.addresses + events = localContact.events + source = SMT_PRIVATE + starred = localContact.starred + contactId = localContact.id!! + thumbnailUri = "" + photo = contactPhoto + notes = localContact.notes + groups = storedGroups.filter { localContact.groups.contains(it.id) } as ArrayList + organization = Organization(localContact.company, localContact.jobPosition) + websites = localContact.websites + IMs = localContact.IMs + } + } + + private fun convertContactToLocalContact(contact: Contact): LocalContact { + return getEmptyLocalContact().apply { + id = if (contact.id == 0) null else contact.id + prefix = contact.prefix + firstName = contact.firstName + middleName = contact.middleName + surname = contact.surname + suffix = contact.suffix + nickname = contact.nickname + photo = getPhotoByteArray(contact.photoUri) + phoneNumbers = contact.phoneNumbers + emails = contact.emails + events = contact.events + starred = contact.starred + addresses = contact.addresses + notes = contact.notes + groups = contact.groups.map { it.id }.toMutableList() as ArrayList + company = contact.organization.company + jobPosition = contact.organization.jobPosition + websites = contact.websites + IMs = contact.IMs + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/QuotedPrintable.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/QuotedPrintable.kt deleted file mode 100644 index e39bf43b..00000000 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/QuotedPrintable.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.simplemobiletools.contacts.pro.helpers - -import java.io.ByteArrayOutputStream -import java.net.URLEncoder - -// https://alvinalexander.com/java/jwarehouse/android/core/java/com/google/android/mms/pdu/QuotedPrintable.java.shtml -object QuotedPrintable { - private const val ESCAPE_CHAR: Byte = '='.toByte() - fun decode(value: String?): String { - val bytes = value?.toByteArray() - if (bytes == null || bytes.isEmpty()) { - return "" - } - - val buffer = ByteArrayOutputStream() - var i = 0 - while (i < bytes.size) { - val b = bytes[i].toInt() - if (b == ESCAPE_CHAR.toInt()) { - try { - if ('\r' == bytes[i + 1].toChar() && '\n' == bytes[i + 2].toChar()) { - i += 3 - continue - } - - val u = Character.digit(bytes[++i].toChar(), 16) - val l = Character.digit(bytes[++i].toChar(), 16) - if (u == -1 || l == -1) { - return "" - } - - buffer.write(((u shl 4) + l).toChar().toInt()) - } catch (e: ArrayIndexOutOfBoundsException) { - return "" - } - - } else { - buffer.write(b) - } - i++ - } - return String(buffer.toByteArray()) - } - - fun encode(value: String): String { - val result = StringBuilder() - value.forEach { - if (it == ' ') { - result.append(' ') - } else { - val urlEncoded = urlEncode(it.toString()) - if (urlEncoded == it.toString()) { - val hex = String.format("%04x", it.toInt()).trimStart('0').toUpperCase() - result.append("=$hex") - } else { - result.append(urlEncoded) - } - } - } - return result.toString() - } - - fun urlEncode(value: String) = URLEncoder.encode(value, "UTF-8").replace("+", " ").replace('%', '=') -} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/VcfExporter.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/VcfExporter.kt index 485010a8..0fc41e3b 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/VcfExporter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/VcfExporter.kt @@ -152,7 +152,7 @@ class VcfExporter { contactsExported++ } - Ezvcard.write(cards).go(file) + Ezvcard.write(cards).go(it) } catch (e: Exception) { activity.showErrorToast(e) } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/VcfImporter.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/VcfImporter.kt index 9102abdb..290327c1 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/VcfImporter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/VcfImporter.kt @@ -6,15 +6,14 @@ import android.provider.ContactsContract.CommonDataKinds import android.widget.Toast import com.simplemobiletools.commons.extensions.showErrorToast import com.simplemobiletools.contacts.pro.activities.SimpleActivity -import com.simplemobiletools.contacts.pro.extensions.dbHelper import com.simplemobiletools.contacts.pro.extensions.getCachePhoto import com.simplemobiletools.contacts.pro.extensions.getCachePhotoUri +import com.simplemobiletools.contacts.pro.extensions.groupsDB +import com.simplemobiletools.contacts.pro.extensions.normalizeNumber import com.simplemobiletools.contacts.pro.helpers.VcfImporter.ImportResult.* import com.simplemobiletools.contacts.pro.models.* import ezvcard.Ezvcard import ezvcard.VCard -import org.joda.time.DateTime -import org.joda.time.format.DateTimeFormat import java.io.File import java.io.FileOutputStream import java.net.URLDecoder @@ -25,8 +24,6 @@ class VcfImporter(val activity: SimpleActivity) { IMPORT_FAIL, IMPORT_OK, IMPORT_PARTIAL } - private val PATTERN = "EEE MMM dd HH:mm:ss 'GMT'ZZ YYYY" - private var contactsImported = 0 private var contactsFailed = 0 @@ -59,7 +56,7 @@ class VcfImporter(val activity: SimpleActivity) { "" } - phoneNumbers.add(PhoneNumber(number, type, label)) + phoneNumbers.add(PhoneNumber(number, type, label, number.normalizeNumber())) } val emails = ArrayList() @@ -112,7 +109,6 @@ class VcfImporter(val activity: SimpleActivity) { val photoData = ezContact.photos.firstOrNull()?.data val photo = null val thumbnailUri = savePhoto(photoData) - val cleanPhoneNumbers = ArrayList() val IMs = ArrayList() ezContact.impps.forEach { @@ -136,7 +132,7 @@ class VcfImporter(val activity: SimpleActivity) { } val contact = Contact(0, prefix, firstName, middleName, surname, suffix, nickname, photoUri, phoneNumbers, emails, addresses, events, - targetContactSource, starred, contactId, thumbnailUri, photo, notes, groups, organization, websites, cleanPhoneNumbers, IMs) + targetContactSource, starred, contactId, thumbnailUri, photo, notes, groups, organization, websites, IMs) // if there is no N and ORG fields at the given contact, only FN, treat it as an organization if (contact.getNameToDisplay().isEmpty() && contact.organization.isEmpty() && ezContact.formattedName.value.isNotEmpty()) { @@ -160,8 +156,10 @@ class VcfImporter(val activity: SimpleActivity) { } private fun formatDateToDayCode(date: Date): String { - val dateTime = DateTime.parse(date.toString(), DateTimeFormat.forPattern(PATTERN)) - return dateTime.toString("yyyy-MM-dd") + val year = 1900 + date.year + val month = String.format("%02d", date.month + 1) + val day = String.format("%02d", date.date) + return "$year-$month-$day" } private fun getContactGroups(ezContact: VCard): ArrayList { @@ -170,7 +168,7 @@ class VcfImporter(val activity: SimpleActivity) { val groupNames = ezContact.categories.values if (groupNames != null) { - val storedGroups = ContactsHelper(activity).getStoredGroups() + val storedGroups = ContactsHelper(activity).getStoredGroupsSync() groupNames.forEach { val groupName = it @@ -179,11 +177,10 @@ class VcfImporter(val activity: SimpleActivity) { if (storedGroup != null) { groups.add(storedGroup) } else { - val newContactGroup = activity.dbHelper.insertGroup(Group(0, groupName)) - - if (newContactGroup != null) { - groups.add(newContactGroup) - } + val newGroup = Group(null, groupName) + val id = activity.groupsDB.insertOrUpdate(newGroup) + newGroup.id = id + groups.add(newGroup) } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/interfaces/ContactsDao.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/interfaces/ContactsDao.kt new file mode 100644 index 00000000..373df2d3 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/interfaces/ContactsDao.kt @@ -0,0 +1,25 @@ +package com.simplemobiletools.contacts.pro.interfaces + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.simplemobiletools.contacts.pro.models.LocalContact + +@Dao +interface ContactsDao { + @Query("SELECT * FROM contacts") + fun getContacts(): List + + @Query("SELECT * FROM contacts WHERE id = :id") + fun getContactWithId(id: Int): LocalContact? + + @Query("UPDATE contacts SET starred = :isStarred WHERE id = :id") + fun updateStarred(isStarred: Int, id: Int) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertOrUpdate(contact: LocalContact): Long + + @Query("DELETE FROM contacts WHERE id = :id") + fun deleteContactId(id: Int) +} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/interfaces/GroupsDao.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/interfaces/GroupsDao.kt new file mode 100644 index 00000000..8991a6f0 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/interfaces/GroupsDao.kt @@ -0,0 +1,19 @@ +package com.simplemobiletools.contacts.pro.interfaces + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.simplemobiletools.contacts.pro.models.Group + +@Dao +interface GroupsDao { + @Query("SELECT * FROM groups") + fun getGroups(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertOrUpdate(group: Group): Long + + @Query("DELETE FROM groups WHERE id = :id") + fun deleteGroupId(id: Long) +} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/models/BlockedNumber.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/models/BlockedNumber.kt new file mode 100644 index 00000000..13fe44fd --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/models/BlockedNumber.kt @@ -0,0 +1,3 @@ +package com.simplemobiletools.contacts.pro.models + +data class BlockedNumber(val id: Long, val number: String, val normalizedNumber: String) diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/models/Contact.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/models/Contact.kt index adad262d..7eb0d57e 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/models/Contact.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/models/Contact.kt @@ -1,17 +1,18 @@ package com.simplemobiletools.contacts.pro.models import android.graphics.Bitmap +import android.telephony.PhoneNumberUtils import com.simplemobiletools.commons.extensions.normalizeString import com.simplemobiletools.commons.helpers.SORT_BY_FIRST_NAME import com.simplemobiletools.commons.helpers.SORT_BY_MIDDLE_NAME import com.simplemobiletools.commons.helpers.SORT_DESCENDING -import com.simplemobiletools.contacts.pro.extensions.applyRegexFiltering +import com.simplemobiletools.contacts.pro.extensions.normalizeNumber +import com.simplemobiletools.contacts.pro.helpers.SMT_PRIVATE -data class Contact(val id: Int, var prefix: String, var firstName: String, var middleName: String, var surname: String, var suffix: String, var nickname: String, +data class Contact(var id: Int, var prefix: String, var firstName: String, var middleName: String, var surname: String, var suffix: String, var nickname: String, var photoUri: String, var phoneNumbers: ArrayList, var emails: ArrayList, var addresses: ArrayList
, - var events: ArrayList, var source: String, var starred: Int, val contactId: Int, val thumbnailUri: String, var photo: Bitmap?, var notes: String, - var groups: ArrayList, var organization: Organization, var websites: ArrayList, var cleanPhoneNumbers: ArrayList, - var IMs: ArrayList) : + var events: ArrayList, var source: String, var starred: Int, var contactId: Int, var thumbnailUri: String, var photo: Bitmap?, var notes: String, + var groups: ArrayList, var organization: Organization, var websites: ArrayList, var IMs: ArrayList) : Comparable { companion object { var sorting = 0 @@ -126,21 +127,19 @@ data class Contact(val id: Int, var prefix: String, var firstName: String, var m fun isABusinessContact() = prefix.isEmpty() && firstName.isEmpty() && middleName.isEmpty() && surname.isEmpty() && suffix.isEmpty() && organization.isNotEmpty() - // do a more advanced phone number check here, compare numbers and and search query with dashes, spaces and everything but numbers removed fun doesContainPhoneNumber(text: String): Boolean { - if (text.isNotEmpty()) { - if (phoneNumbers.any { it.value.contains(text) } || cleanPhoneNumbers.any { it.value.contains(text) }) { - return true + return if (text.isNotEmpty()) { + val normalizedText = text.normalizeNumber() + phoneNumbers.any { + PhoneNumberUtils.compare(it.normalizedNumber, normalizedText) || + it.value.contains(text) || + it.normalizedNumber?.contains(normalizedText) == true || + it.value.normalizeNumber().contains(normalizedText) } + } else { + false } - - val filteredNumber = text.applyRegexFiltering() - if (filteredNumber.isNotEmpty()) { - if (phoneNumbers.any { it.value.contains(filteredNumber) } || cleanPhoneNumbers.any { it.value.contains(filteredNumber) }) { - return true - } - } - - return false } + + fun isPrivate() = source == SMT_PRIVATE } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/models/Group.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/models/Group.kt index c36635bf..9628e664 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/models/Group.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/models/Group.kt @@ -1,16 +1,21 @@ package com.simplemobiletools.contacts.pro.models +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey import com.simplemobiletools.contacts.pro.helpers.FIRST_GROUP_ID import java.io.Serializable -data class Group(var id: Long, var title: String, var contactsCount: Int = 0) : Serializable { - companion object { - private const val serialVersionUID = -1384515348451345L - } +@Entity(tableName = "groups", indices = [(Index(value = ["id"], unique = true))]) +data class Group( + @PrimaryKey(autoGenerate = true) var id: Long?, + @ColumnInfo(name = "title") var title: String, + @ColumnInfo(name = "contacts_count") var contactsCount: Int = 0) : Serializable { fun addContact() = contactsCount++ fun getBubbleText() = title - fun isPrivateSecretGroup() = id >= FIRST_GROUP_ID + fun isPrivateSecretGroup() = id ?: 0 >= FIRST_GROUP_ID } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/models/LocalContact.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/models/LocalContact.kt new file mode 100644 index 00000000..a2947246 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/models/LocalContact.kt @@ -0,0 +1,33 @@ +package com.simplemobiletools.contacts.pro.models + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity(tableName = "contacts", indices = [(Index(value = ["id"], unique = true))]) +data class LocalContact( + @PrimaryKey(autoGenerate = true) var id: Int?, + @ColumnInfo(name = "prefix") var prefix: String, + @ColumnInfo(name = "first_name") var firstName: String, + @ColumnInfo(name = "middle_name") var middleName: String, + @ColumnInfo(name = "surname") var surname: String, + @ColumnInfo(name = "suffix") var suffix: String, + @ColumnInfo(name = "nickname") var nickname: String, + @ColumnInfo(name = "photo", typeAffinity = ColumnInfo.BLOB) var photo: ByteArray?, + @ColumnInfo(name = "phone_numbers") var phoneNumbers: ArrayList, + @ColumnInfo(name = "emails") var emails: ArrayList, + @ColumnInfo(name = "events") var events: ArrayList, + @ColumnInfo(name = "starred") var starred: Int, + @ColumnInfo(name = "addresses") var addresses: ArrayList
, + @ColumnInfo(name = "notes") var notes: String, + @ColumnInfo(name = "groups") var groups: ArrayList, + @ColumnInfo(name = "company") var company: String, + @ColumnInfo(name = "job_position") var jobPosition: String, + @ColumnInfo(name = "websites") var websites: ArrayList, + @ColumnInfo(name = "ims") var IMs: ArrayList) { + + override fun equals(other: Any?) = id == (other as? LocalContact?)?.id + + override fun hashCode() = id ?: 0 +} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/models/PhoneNumber.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/models/PhoneNumber.kt index b95127aa..3c0850c3 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/models/PhoneNumber.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/models/PhoneNumber.kt @@ -1,3 +1,3 @@ package com.simplemobiletools.contacts.pro.models -data class PhoneNumber(var value: String, var type: Int, var label: String) +data class PhoneNumber(var value: String, var type: Int, var label: String, var normalizedNumber: String?) diff --git a/app/src/main/res/drawable-hdpi/ic_block.png b/app/src/main/res/drawable-hdpi/ic_block.png new file mode 100644 index 00000000..1c541fe5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_block.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_block.png b/app/src/main/res/drawable-xhdpi/ic_block.png new file mode 100644 index 00000000..ae3856fe Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_block.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_block.png b/app/src/main/res/drawable-xxhdpi/ic_block.png new file mode 100644 index 00000000..d6dc2222 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_block.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_block.png b/app/src/main/res/drawable-xxxhdpi/ic_block.png new file mode 100644 index 00000000..9f1191c9 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_block.png differ diff --git a/app/src/main/res/layout/activity_edit_contact.xml b/app/src/main/res/layout/activity_edit_contact.xml index ecf79006..304a7a52 100644 --- a/app/src/main/res/layout/activity_edit_contact.xml +++ b/app/src/main/res/layout/activity_edit_contact.xml @@ -4,7 +4,8 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/contact_scrollview" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:visibility="gone"> + + + + + + + + + diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 167eb122..9950dfd3 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -78,6 +78,28 @@ + + + + + + + + + + + + + android:layout_height="wrap_content" + android:visibility="gone"> + + + + + diff --git a/app/src/main/res/layout/item_manage_blocked_number.xml b/app/src/main/res/layout/item_manage_blocked_number.xml new file mode 100644 index 00000000..e78f8724 --- /dev/null +++ b/app/src/main/res/layout/item_manage_blocked_number.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/layout/item_view_group.xml b/app/src/main/res/layout/item_view_group.xml index 8aed14d6..12959127 100644 --- a/app/src/main/res/layout/item_view_group.xml +++ b/app/src/main/res/layout/item_view_group.xml @@ -5,10 +5,11 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerVertical="true" + android:background="?attr/selectableItemBackground" android:ellipsize="end" android:lines="1" android:maxLines="1" - android:paddingBottom="@dimen/normal_margin" android:paddingTop="@dimen/normal_margin" + android:paddingBottom="@dimen/normal_margin" android:singleLine="true" android:textSize="@dimen/bigger_text_size"/> diff --git a/app/src/main/res/menu/cab_recent_calls.xml b/app/src/main/res/menu/cab_recent_calls.xml index 128e6362..f5e7c4ba 100644 --- a/app/src/main/res/menu/cab_recent_calls.xml +++ b/app/src/main/res/menu/cab_recent_calls.xml @@ -11,4 +11,9 @@ android:icon="@drawable/ic_select_all" android:title="@string/select_all" app:showAsAction="ifRoom"/> + diff --git a/app/src/main/res/menu/menu_add_blocked_number.xml b/app/src/main/res/menu/menu_add_blocked_number.xml new file mode 100644 index 00000000..fb290efd --- /dev/null +++ b/app/src/main/res/menu/menu_add_blocked_number.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index 8056c20e..fd4f43b3 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -17,11 +17,14 @@ Lazım olan icazələri istə Create new contact Add to an existing contact + You have to make this app the default dialer app to make use of blocked numbers. + Set to default No contacts found No contacts with emails have been found No contacts with phone numbers have been found + No recent calls found Yeni kontakt Redaktə et @@ -52,6 +55,7 @@ Ada soyaddan başla Telefon nömrələrini əsas ekranda göstər Kontakt görüntülərini göstər + Show a dialpad button on the main screen Kontakta toxunduqda Kontakta zəng et Kontakt detallarına bax @@ -107,6 +111,16 @@ Dialpad Add number to contact + + Dialer + Calling + Incoming call + Incoming call from… + Ongoing call + Disconnected + Decline + Answer + Göstərmək üçün sahəni seç Ön şəkilçi @@ -122,6 +136,14 @@ Kontakt kökü Instant messaging (IM) + + Manage blocked numbers + You are not blocking anyone. + Add a blocked number + Block number + Block numbers + Blocked numbers + I want to change what fields are visible at contacts. Can I do it? Yes, all you have to do is go in Settings -> Manage shown contact fields. There you can select what fields should be visible. Some of them are even disabled by default, so you might find some new ones there. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index aa9e30e1..d031d5b3 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -17,11 +17,14 @@ Benötigte Berechtigungen anfordern Neuen Kontakt erstellen Zu einem existierenden Kontakt hinzufügen + You have to make this app the default dialer app to make use of blocked numbers. + Set to default - No contacts found - No contacts with emails have been found - No contacts with phone numbers have been found + Keine Kontakte gefunden + Keine Kontakte mit E-Mailadressen gefunden + Keine Kontakte mit Telefonnummern gefunden + No recent calls found Neuer Kontakt Kontakt bearbeiten @@ -52,6 +55,7 @@ Namen mit Nachnamen beginnen Telefonnummern im Hauptmenü zeigen Vorschaubilder der Kontakte zeigen + Show a dialpad button on the main screen Beim Klicken auf den Kontakt Kontakt anrufen Kontaktdetails anzeigen @@ -62,7 +66,7 @@ Favoriten Anrufliste Bestätigungsdialog zeigen, bevor ein Anruf durchgeführt wird - Show only contacts with phone numbers + Nur Kontakte mit Telefonnummern anzeigen E-Mail @@ -107,6 +111,16 @@ Wählfeld Nummer zu Kontakt hinzufügen + + Dialer + Calling + Incoming call + Incoming call from… + Ongoing call + Disconnected + Decline + Answer + Sichtbare Felder auswählen Titel @@ -122,6 +136,14 @@ Kontaktquelle Instant messaging (IM) + + Manage blocked numbers + You are not blocking anyone. + Add a blocked number + Block number + Block numbers + Blocked numbers + Ich möchte die sichtbaren Kontaktfelder ändern. Kann ich das machen? Ja, alles, was Sie tun müssen ist folgendes: Gehen Sie zu Einstellungen -> Bearbeite sichtbare Kontaktfelder. Hier können die sichtbaren Felder ausgewählt werden. Einige sind standardmäßig deaktiviert, weshalb hier neue gefunden werden können. diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 618c88fb..b18b448e 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -17,11 +17,14 @@ Ζητούνται τα απαιτούμενα δικαιώματα Δημιουργία νέας Επαφής Προσθήκη σε μια υπάρχουσα Επαφή + You have to make this app the default dialer app to make use of blocked numbers. + Set to default No contacts found No contacts with emails have been found No contacts with phone numbers have been found + No recent calls found Νέα επαφή Επεξεργασία επαφής @@ -52,6 +55,7 @@ Εμφάνιση πρώτα το επώνυμο Εμφάνιση τηλεφωνικών αριθμών στην κύρια οθόνη Εμφάνιση μικρογραφιών επαφής + Show a dialpad button on the main screen Στην επιλογή επαφής Κλήση επαφής Εμφάνιση λεπτομερειών επαφής @@ -107,6 +111,16 @@ Πληκτρολόγιο Προσθήκη αριθμού σε επαφή + + Dialer + Calling + Incoming call + Incoming call from… + Ongoing call + Disconnected + Decline + Answer + Επιλογή εμφάνισης πεδίων Πρόθεμα @@ -122,6 +136,14 @@ Προέλευση επαφής Αμεσο μήνυμα (IM) + + Manage blocked numbers + You are not blocking anyone. + Add a blocked number + Block number + Block numbers + Blocked numbers + Θέλω να αλλάξω τα πεδία που θα είναι ορατά στις επαφές. Μπορώ να το κάνω? Ναι, το μόνο που έχετε να κάνετε είναι να μεταβείτε στις Ρυθμίσεις -> Διαχείριση εμφανιζόμενων πεδίων επαφής. Εκεί μπορείτε να επιλέξετε ποια πεδία θα πρέπει να είναι ορατά. Κάποια από αυτά είναι ακόμη και απενεργοποιημένα από προεπιλογή, επομένως ίσως βρείτε κάποια νέα εκεί. diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index fab37fca..da6af990 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -2,8 +2,8 @@ Kontaktu sinpleak Kontaktuak Helbidea - Txertatzen... - Eguneratzen... + Txertatzen… + Eguneratzen… Telefono memoria Telefono memoria (beste aplikazioentzat ikustezina) Enpresa @@ -15,6 +15,16 @@ Bidali emaila taldeari %s deitu Eskatu beharrezko baimenak + Create new contact + Add to an existing contact + You have to make this app the default dialer app to make use of blocked numbers. + Set to default + + + No contacts found + No contacts with emails have been found + No contacts with phone numbers have been found + No recent calls found Kontaktu berria Editatu taldea @@ -23,6 +33,7 @@ Izena Erdiko izena Abizena + Nickname Talderik ez @@ -43,6 +54,8 @@ Abizenaren arabera sailkatu Erakutsi telefono zenbakiak pantaila nagusian + Show contact thumbnails + Show a dialpad button on the main screen Kontaktu sakatzean Kontaktua deitu Ikusi kontaktu detaileak @@ -53,6 +66,7 @@ Gogokoak Azken deiak Erakutsi egiaztatze mezua dei bat hasi baino lehen + Show only contacts with phone numbers Emaila @@ -66,6 +80,7 @@ Nagusia Laneko faxa Etxeko faxa + Pager Ez da telefono zenbakirik aurkitu @@ -88,8 +103,24 @@ Esportatu kontaktuak Inportatu .vcf fitxategiko kontaktuak Esportatu kontaktua .vcf fitxategi batera + Target contact source + Include contact sources Fitxategi izena (.vcf gabe) + + Dialpad + Add number to contact + + + Dialer + Calling + Incoming call + Incoming call from… + Ongoing call + Disconnected + Decline + Answer + Hautatu erakusteko eremuak Aurrizkia @@ -103,6 +134,15 @@ Webguneak Taldeak Kontaktu jatorria + Instant messaging (IM) + + + Manage blocked numbers + You are not blocking anyone. + Add a blocked number + Block number + Block numbers + Blocked numbers Aldatu ditzaket kontaktuetan ikusgarri dauden eremuak? diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d51d9d93..093f665a 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -2,26 +2,29 @@ Simple Contacts Contacts Adresse - Ajout… - Mise à jour… + Ajout en cours + Actualisation en cours Stockage du téléphone - Stockage du téléphone (non visible par d\'autres applis) - Société + Stockage du téléphone (non visible par d\'autres applications) + Entreprise Poste - Site web + Site Internet Envoyer un SMS aux contacts - Envoyer un e-mail aux contacts + Envoyer un courriel aux contacts Envoyer un SMS au groupe - Envoyer un e-mail au groupe - Call %s - Demander les autorisations requises + Envoyer un courriel au groupe + Appeler %s + Demander les autorisations nécessaires Créer un nouveau contact Ajouter à un contact existant + You have to make this app the default dialer app to make use of blocked numbers. + Set to default - No contacts found - No contacts with emails have been found - No contacts with phone numbers have been found + Aucun contact n\'a été trouvé + Aucun contact avec une adresse de courriel n\'a été trouvé + Aucun contact avec un numéro de téléphone n\'a été trouvé + Aucun appel récent n\'a été trouvé Nouveau contact Modifier contact @@ -33,15 +36,15 @@ Surnom - Pas de groupe + Aucun groupe Créer un nouveau groupe - Enlever du groupe + Supprimer du groupe Ce groupe est vide - Ajout contacts - Il n\'y a pas de groupes de contacts sur l\'appareil + Ajouter des contacts + Aucun groupe de contacts sur l\'appareil Créer un groupe Ajouter à un groupe - Créer un groupe pris en compte + Créer un groupe dans le compte Prendre une photo @@ -52,20 +55,21 @@ Trier les contacts par nom de famille Afficher les numéros de téléphone Afficher les vignettes des contacts - Sur appui du contact + Afficher un bouton \"Clavier téléphonique\" sur l\'écran principal + Lors d\'un appui sur un contact Appeler le contact - Voir les détails du contact + Afficher les détails du contact Configurer l\'affichage des champs des contacts Essayez de filtrer les contacts en double Gérer les onglets affichés Contacts - Favorites + Favoris Appels récents - Afficher une boîte de dialogue de confirmation d\'appel avant de lancer un appel - Show only contacts with phone numbers + Afficher une demande de confirmation avant de démarrer un appel + Afficher uniquement les contacts avec un numéro de téléphone - E-mail + Adresse de courriel Maison Travail Autre @@ -84,11 +88,11 @@ Anniversaire - Vous n\'ayez pas encore ajouté de contact favori. + Aucun contact favori n\'a été trouvé Ajouter des favoris Ajouter aux favoris - Retirer des favoris - Vous devez être sur l\'écran Modifier pour modifier un contact + Supprimer des favoris + Vous devez être sur l\'écran \"Modifier\" pour modifier un contact Rechercher des contacts @@ -99,44 +103,62 @@ Exporter des contacts Importer des contacts depuis un fichier .vcf Exporter des contacts vers un fichier .vcf - Source du contact cible - Inclure les sources du contact + Compte pour le du contact destinataire + Ajouter le compte pour le contact Nom du fichier (sans .vcf) - Dialpad + Clavier téléphonique Ajouter un numéro au contact + + Numéroteur + Appel en cours + Appel entrant + Appel entrant de… + Appel en cours + Interrompu + Refuser + Répondre + Sélectionner les champs à afficher Préfixe Suffixe Numéros de téléphone - E-mails + Adresse de courriels Adresses Évènements (naissances, anniversaires) Notes Organisation - Sites web + Sites Internet Groupe - Source du contact + Compte pour le contact Messagerie instantanée (MI) + + Manage blocked numbers + You are not blocking anyone. + Add a blocked number + Block number + Block numbers + Blocked numbers + - Je veux changer quelles champs sont visibles. Est-ce que je peux ? - Oui, tout ce que vous avez à faire c\'est d\'aller dans Paramètres -> Configurer l\'affichage des champs de contact. Ici vous pouvez sélectionner quelles champs vous voulez afficher. Certains sont désactivés par défaut, ainsi vous pourrez y trouver des nouveaux champs. + Je veux modifier les champs affichés sur les fiches de mes contacts. Puis-je le faire ? + Oui, tout ce que vous avez à faire c\'est d\'aller dans \"Paramètres\" -> \"Configurer l\'affichage des champs de contact\". Sélectionnez les champs à afficher. Certains champs sont désactivés par défaut. - Une appli de contacts pour gérer vos contacts sans pubs. + Une application répertoire pour gérer vos contacts sans publicité. - Une appli simple pour créer et gérer vos contacts depuis n\'importe quelle source. Les contacts peuvent être stockés sur votre appareil mais aussi synchronisés via Google ou d\'autres comptes. Vous pouvez afficher vos contacts favoris dans une liste séparée. + Un outil simple pour créer et gérer vos contacts depuis n\'importe quelle source. Les contacts peuvent être stockés sur votre appareil mais aussi synchronisés via votre compte Google ou d\'autres comptes. Vous pouvez afficher vos contacts favoris dans une liste séparée. - Vous pouvez l\'utiliser pour gérer les e-mail et événements de vos contacts. Elle permet de trier/filter via de multiples paramètres, et même afficher le surnom en premier. + Vous pouvez l\'utiliser pour gérer les adresses de courriels et les événements de vos contacts. Cet outil permet de trier/filtrer à l\'aide de multiples paramètres, par exemple : afficher le surnom en premier. - Aucune publicité ni de permission inutile. Elle est entièrement open source et vous permet de personnaliser les couleurs. + L\'application ne contient ni publicité, ni autorisation inutile. Elle est totalement opensource et est également fournie avec des couleurs personnalisables. - Cette application fait parti d\'un groupe d\'applications. Vous pouvez trouver le reste des applis sur https://www.simplemobiletools.com + Cette application fait partie d\'une plus grande suite. Vous pouvez trouver les autres applications sur https://www.simplemobiletools.com No contacts found No contacts with emails have been found No contacts with phone numbers have been found + No recent calls found Novi kontakt Uredi kontakt @@ -52,6 +55,7 @@ Započnite imena s prezimenima Prikaži telefonske brojeve na glavnom zaslonu Prikaži sličice kontakata + Show a dialpad button on the main screen Prilikom dodira kontakta Nazovi kontakt Prikaži pojedinosti o kontaktu @@ -107,6 +111,16 @@ Dialpad Add number to contact + + Dialer + Calling + Incoming call + Incoming call from… + Ongoing call + Disconnected + Decline + Answer + Odaberi polja za prikaz Prefiks @@ -122,6 +136,14 @@ Izvori kontakata Brzo slanje poruka (IM) + + Manage blocked numbers + You are not blocking anyone. + Add a blocked number + Block number + Block numbers + Blocked numbers + Želim promijeniti polja koja su vidljiva na kontaktima. Mogu li to napraviti? Da, sve što morate učiniti je otići u Postavke -> Upravljanje poljima za prikaz. Tamo možete odabrati polja koja bi trebala biti vidljiva. Neka od njih su čak i onemogućena prema zadanim postavkama, tako da možete pronaći neke nove. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index dd4efaa4..49dde94e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -5,7 +5,7 @@ Inserimento in corso… Aggiornamento in corso… Memoria del telefono - Memoria del telfono (non visibile alle altre applicazioni) + Memoria del telefono (non visibile alle altre applicazioni) Compagnia Posizione lavorativa Sito web @@ -17,11 +17,14 @@ Richiedi le permissioni necessarie Crea un nuovo contatto Aggiungi ad un contatto esistente + È necessario impostare quest\'app come predefinita per utilizzare i numeri bloccati. + Imposta come predefinita Nessun contatto trovato Nessun contatto trovato con un\'email Nessun contatto trovato con un numero di telefono + Nessuna chiamata recente trovata Nuovo contatto Modifica contatto @@ -52,11 +55,12 @@ Prima il nome poi il cognome Mostra i numeri di telefono nella schermata principale Mostra le anteprime dei contatti + Mostra il pulante per la tastiera nello schermo principale Al click sul contatto Chiama contatto Visualizza i dettagli del contatto Gestisci i campi mostrati - Prova a filetrare i contatti duplicati + Prova a filtrare i contatti duplicati Gestisci le schede mostrate Contatti Preferiti @@ -107,6 +111,16 @@ Tastiera Aggiungi numero ai contatti + + Compositore + Chiamata in corso + Chiamata in arrivo + Chiamata in arrivo da… + Chiamata in corso + Disconnesso + Rifiuta + Rispondi + Seleziona i campi da mostrare Prefisso @@ -122,6 +136,14 @@ Provenienza del contatto Messaggistica istantanea (IM) + + Gestisci i numeri bloccati + Non si sta blocccando alcun numero. + Aggiungi un numero da bloccare + Blocca numero + Blocca numeri + Numeri bloccati + Voglio cambiare i campi visibili ai contatti. Come posso fare? Puoi farlo andando in Impostazioni -> Gestisci i campi mostrati. Qui puoi selezionare i campi che saranno visibili. Alcuni sono anche disabilitati in maniera predefinita, quindi potresti trovare qualche nuovo campo. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index d22b12f6..fec96b80 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -17,11 +17,14 @@ Request the required permissions 新しい連絡先を作成 既存の連絡先に追加 + You have to make this app the default dialer app to make use of blocked numbers. + Set to default 連絡先が見つかりません メールアドレスが登録された連絡先が見つかりません 電話番号が登録された連絡先が見つかりません + 通話履歴はありません 新しい連絡先 連絡先を編集 @@ -52,6 +55,7 @@ 姓を先に表示 メイン画面に電話番号を表示 連絡先のサムネイルを表示 + メイン画面にダイヤルパッドを表示 連絡先をタップ 連絡先に発信 連絡先の詳細を表示 @@ -60,7 +64,7 @@ 表示するタブを管理 連絡先 お気に入り - Recent calls + 通話履歴 発信する前に確認ダイアログを表示する 電話番号が登録された連絡先のみ表示する @@ -107,6 +111,16 @@ ダイヤルパッド 連絡先に番号を追加 + + 電話 + 発信中 + 着信中 + 着信中… + 通話中 + 切断されました + 拒否 + 応答 + 表示する項目を選択 敬称(名前の前) @@ -122,6 +136,14 @@ インポート元 Instant messaging (IM) + + ブロックした番号を管理 + まだ誰もブロックしていません. + ブロックする番号を追加 + Block number + Block numbers + Blocked numbers + 連絡先に表示される項目(フィールド)を変更することはできますか? 可能です。[設定] -> [連絡先に表示するフィールドを管理] から、表示されるフィールドを選択することができます。これらの中にはデフォルトで無効になっているものもあるので、あなたは新しいものを見つけるかもしれません。 diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 12228256..38dc1ab2 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -17,11 +17,14 @@ Request the required permissions Create new contact Add to an existing contact + You have to make this app the default dialer app to make use of blocked numbers. + Set to default No contacts found No contacts with emails have been found No contacts with phone numbers have been found + No recent calls found 새로운 연락처 연락처 수정 @@ -52,6 +55,7 @@ 성을 먼저 표시 메인 스크린에 전화번호 표시 Show contact thumbnails + Show a dialpad button on the main screen On contact click Call contact View contact details @@ -107,6 +111,16 @@ Dialpad Add number to contact + + Dialer + Calling + Incoming call + Incoming call from… + Ongoing call + Disconnected + Decline + Answer + Select fields to show Prefix @@ -122,6 +136,14 @@ Contact source Instant messaging (IM) + + Manage blocked numbers + You are not blocking anyone. + Add a blocked number + Block number + Block numbers + Blocked numbers + I want to change what fields are visible at contacts. Can I do it? Yes, all you have to do is go in Settings -> Manage shown contact fields. There you can select what fields should be visible. Some of them are even disabled by default, so you might find some new ones there. diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index e29dad0f..471adfb6 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -17,11 +17,14 @@ Request the required permissions Create new contact Add to an existing contact + You have to make this app the default dialer app to make use of blocked numbers. + Set to default No contacts found No contacts with emails have been found No contacts with phone numbers have been found + No recent calls found Naujas kontaktas Redaguoti kontaktą @@ -52,6 +55,7 @@ Pavardė rodoma pirma Rodyti telefono numerius pagrindiniame programos ekrane Rodyti kontaktų miniatiūras + Show a dialpad button on the main screen Ant kontakto paspaudimo Skambinti kontaktui Žiūrėti kontakto detales @@ -107,6 +111,16 @@ Dialpad Add number to contact + + Dialer + Calling + Incoming call + Incoming call from… + Ongoing call + Disconnected + Decline + Answer + Pasirinkti rodomus laukelius Priešdėlis @@ -122,6 +136,14 @@ Kontakto šaltinis Instant messaging (IM) + + Manage blocked numbers + You are not blocking anyone. + Add a blocked number + Block number + Block numbers + Blocked numbers + Noriu pakeisti, kokie laukai yra matomi kontaktuose. Ar galiu tai padaryti? Taip, viskas, ką jums reikia padaryti, tai eiti į Nustatymai -> Tvarkyti rodomus kontaktų laukus. Čia galite pasirinkti, kurie laukai turėtų būti matomi. Kai kurie iš jų netgi yra išjungiami pagal numatytuosius nustatymus, todėl ten galite rasti naujų. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 8d3ee2c4..d185d0fd 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -17,11 +17,14 @@ Pedir a permissão necessária Criar novo contacto Adicionar a contacto existente + You have to make this app the default dialer app to make use of blocked numbers. + Set to default - No contacts found - No contacts with emails have been found - No contacts with phone numbers have been found + Não existem contactos + Não existem contactos com endereço de e-mail + Não existem contactos com número de telefone + Não existem chamadas recentes Novo contacto Editar contacto @@ -33,7 +36,7 @@ Alcunha - Não há grupos + Não existem grupos Criar um novo grupo Remover do grupo Este grupo está vazio @@ -52,17 +55,18 @@ Ordenar por apelido Mostrar número de telefone no ecrã principal Mostrar miniatura do contacto + Mostrar botão Marcador no ecrã principal Ao tocar no contacto Ligar Ver detalhes - Gerir campos a exibir + Gerir campos a mostrar Tentar filtrar contactos duplicados Gerir separadores a exibir Contactos Favoritos Chamadas recentes Mostrar diálogo para confirmar a chamada - Show only contacts with phone numbers + Mostrar apenas contactos com número de telefone E-mail @@ -104,9 +108,19 @@ Nome do ficheiro (sem .vcf) - Teclado + Marcador Adicionar número a um contacto + + Marcador + A chamar + Chamada recebida + Chamada recebida de… + Chamada efetuada + Desligada + Recusar + Atender + Selecione os campos a mostrar Prefixo @@ -122,6 +136,14 @@ Origem do contacto Mensagem instantânea (IM) + + Manage blocked numbers + You are not blocking anyone. + Add a blocked number + Block number + Block numbers + Blocked numbers + I want to change what fields are visible at contacts. Can I do it? Yes, all you have to do is go in Settings -> Manage shown contact fields. There you can select what fields should be visible. Some of them are even disabled by default, so you might find some new ones there. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 6d5c8629..7bab0c68 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -17,11 +17,14 @@ Запрос необходимых разрешений Создать новый контакт Добавить к существующему контакту + You have to make this app the default dialer app to make use of blocked numbers. + Set to default Контакты не найдены Контакты с адресами электронной почты не найдены Контакты с номерами телефонов не найдены + No recent calls found Новый контакт Редактировать контакт @@ -52,6 +55,7 @@ Показывать сначала фамилию Показывать номера телефонов на главном экране Показывать фото контакта + Show a dialpad button on the main screen При нажатии на контакт Позвонить контакту Просмотреть подробности о контакте @@ -107,6 +111,16 @@ Набор номера Добавить номер к контакту + + Dialer + Calling + Incoming call + Incoming call from… + Ongoing call + Disconnected + Decline + Answer + Выберите отображаемые поля Префикс @@ -122,6 +136,14 @@ Источник контакта Мессенджеры (IM) + + Manage blocked numbers + You are not blocking anyone. + Add a blocked number + Block number + Block numbers + Blocked numbers + Я хочу изменить поля, отображаемые у контактов. Могу ли я это сделать? Да, всё, что вам нужно сделать, это зайти в \"Настройки\" -> \"Управление отображаемыми полями контактов\". Там вы сможете выбрать, какие поля должны быть видимы. Некоторые из них даже отключены по умолчанию, так что вы можете найти некоторые новые. diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 77d6cfe2..614febb0 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -17,11 +17,14 @@ Vyžiadať potrebné oprávnenia Vytvoriť nový kontakt Pridať k existujúcemu kontaktu + Pre použitie blokovania čísel musíte nastaviť aplikáciu ako predvolenú pre správu hovorov. + Nastaviť ako predvolenú Nenašli sa žiadne kontakty Nenašli sa žiadne kontakty s emailami Nenašli sa žiadne kontakty s telefónnymi číslami + Nenašli sa žiadne posledné hovory Nový kontakt Upraviť kontakt @@ -52,6 +55,7 @@ Začať meno priezviskom Zobraziť telefónne čísla na hlavnej obrazovke Zobraziť obrázky kontaktov + Zobraziť tlačidlo pre číselník na hlavnej obrazovke Po kliknutí na kontakt Zavolať kontakt Zobraziť údaje kontaktu @@ -107,6 +111,16 @@ Číselník Pridať číslo kontaktu + + Telefón + Vytáča sa + Prichádzajúci hovor + Prichádzajúci hovor od… + Prebiehajúci hovor + Ukončený hovor + Odmietnuť + Prijať + Zvoľte polia na zobrazenie Titul pred menom @@ -122,6 +136,14 @@ Zdroje kontaktov Rýchle správy (IM) + + Spravovať blokované čísla + Neblokujete nikoho. + Pridať blokované číslo + Blokovať číslo + Blokovať čísla + Blokované čísla + Chcem upraviť viditeľné polia kontaktov. Dá sa to? Áno, stačí ísť do Nastavenia -> Spravovať zobrazené polia kontaktov. Tam si viete zvoliť, ktoré polia majú byť viditeľné. Niektoré sú v predvolenom stave vypnuté, čiže tam môžete objaviť aj nové. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 53652727..43dba3f3 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -17,11 +17,14 @@ Begär de behörigheter som krävs Skapa ny kontakt Lägg till i en befintlig kontakt + You have to make this app the default dialer app to make use of blocked numbers. + Set to default Inga kontakter hittades Inga kontakter med e-postadresser hittades Inga kontakter med telefonnummer hittades + No recent calls found Ny kontakt Redigera kontakt @@ -52,6 +55,7 @@ Visa efternamn först Visa telefonnummer i huvudvyn Visa kontaktminiatyrer + Show a dialpad button on the main screen Vid kontakttryckning Ring kontakt Visa kontaktuppgifter @@ -107,6 +111,16 @@ Knappsats Lägg till nummer i kontakt + + Dialer + Calling + Incoming call + Incoming call from… + Ongoing call + Disconnected + Decline + Answer + Välj vilka fält som ska visas Prefix @@ -122,6 +136,14 @@ Kontaktkälla Snabbmeddelanden (IM) + + Manage blocked numbers + You are not blocking anyone. + Add a blocked number + Block number + Block numbers + Blocked numbers + I want to change what fields are visible at contacts. Can I do it? Yes, all you have to do is go in Settings -> Manage shown contact fields. There you can select what fields should be visible. Some of them are even disabled by default, so you might find some new ones there. diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index f94da90e..15d9bf3d 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -17,11 +17,14 @@ Gerekli izinleri iste Yeni kişi oluştur Mevcut bir kişiye ekle + You have to make this app the default dialer app to make use of blocked numbers. + Set to default Kişi bulunamadı E-posta ile hiç bağlantı bulunamadı Telefon numaralarını içeren kişi bulunamadı + No recent calls found Yeni kişi Kişiyi düzenle @@ -52,6 +55,7 @@ Soyadı ile başla Ana ekranda telefon numaralarını göster Kişi küçük resimlerini göster + Show a dialpad button on the main screen Kişi tıklandığında Kişiyi ara Kişi bilgilerini göster @@ -107,6 +111,16 @@ Tuş takımı Kişiye numara ekle + + Dialer + Calling + Incoming call + Incoming call from… + Ongoing call + Disconnected + Decline + Answer + Görüntülenecek alanları seç Önek @@ -122,6 +136,14 @@ Kişi kaynağı Anlık mesajlaşma (IM) + + Manage blocked numbers + You are not blocking anyone. + Add a blocked number + Block number + Block numbers + Blocked numbers + Rehberde görüntülenecek alanları değiştirmek istiyorum. Bunu yapabilir miyim? Evet, tek yapmanız gereken Ayarlar -> Görüntülenecek kişi alanlarını yönet\'e gitmek. Orada hangi alanların görüntüleneceğini seçebilirsiniz. Bazıları varsayılan olarak devre dışı bile olsa, orada bazı yenilerini bulabilirsiniz. diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 3f730034..9be3d686 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -17,11 +17,14 @@ 請求必要的權限 建立新聯絡人 添加至已存在的聯絡人 + 你必須將這應用程式設為預設的撥號程式來使用黑名單。 + 設為預設 - No contacts found - No contacts with emails have been found - No contacts with phone numbers have been found + 未發現聯絡人 + 未發現含有電子信箱的聯絡人 + 未發現含有電話號碼的聯絡人 + 未發現通話紀錄 新聯絡人 編輯聯絡人 @@ -52,6 +55,7 @@ 姓氏在前 主畫面顯示電話號碼 顯示聯絡人縮圖 + 在主畫面顯示撥號按鈕 點擊聯絡人 打電話給聯絡人 顯示聯絡人資料 @@ -62,7 +66,7 @@ 我的最愛 通話紀錄 開始通話前顯示通話確認框 - Show only contacts with phone numbers + 只顯示含有電話話碼的聯絡人 電子信箱 @@ -107,6 +111,16 @@ 撥號畫面 添加號碼至通訊錄 + + 撥號器 + 撥號中 + 來電 + 通話來自於… + 持續通話 + 未接電話 + 掛斷電話 + 回撥 + 選擇要顯示的欄位 前缀 @@ -122,6 +136,14 @@ 聯絡人來源 即時通訊 (IM) + + 管理黑名單 + 你沒有封鎖任何人 + 添加封鎖的號碼 + 封鎖號碼 + 封鎖號碼 + 黑名單 + 我想要更改在通訊錄會看到哪些欄位。我能這麼做嗎? 可以,你要做的是到[設定] -> [管理顯示的聯絡人欄位]。在那裡,你可以選擇應該看到什麼欄位。其中有些甚至預設是關閉的,所以你可能會在那裡發現一些新的。 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml deleted file mode 100644 index d781ec5f..00000000 --- a/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index b0496177..e5a33be5 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -7,5 +7,6 @@ 56dp 68dp 60dp + 34sp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8bf9c12c..af52d0b9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,11 +17,14 @@ Request the required permissions Create new contact Add to an existing contact + You have to make this app the default dialer app to make use of blocked numbers. + Set to default No contacts found No contacts with emails have been found No contacts with phone numbers have been found + No recent calls found New contact Edit contact @@ -52,6 +55,7 @@ Start name with surname Show phone numbers on the main screen Show contact thumbnails + Show a dialpad button on the main screen On contact click Call contact View contact details @@ -107,6 +111,16 @@ Dialpad Add number to contact + + Dialer + Calling + Incoming call + Incoming call from… + Ongoing call + Disconnected + Decline + Answer + Select fields to show Prefix @@ -122,6 +136,14 @@ Contact source Instant messaging (IM) + + Manage blocked numbers + You are not blocking anyone. + Add a blocked number + Block number + Block numbers + Blocked numbers + I want to change what fields are visible at contacts. Can I do it? Yes, all you have to do is go in Settings -> Manage shown contact fields. There you can select what fields should be visible. Some of them are even disabled by default, so you might find some new ones there. diff --git a/build.gradle b/build.gradle index 4ddb226d..85c5b5af 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.0' + ext.kotlin_version = '1.3.10' repositories { google() diff --git a/keystore.properties_sample b/keystore.properties_sample new file mode 100644 index 00000000..569edd73 --- /dev/null +++ b/keystore.properties_sample @@ -0,0 +1,4 @@ +storePassword=123456 +keyPassword=abcdef +keyAlias=myAlias +storeFile=../keystore.jks diff --git a/signing.properties_sample b/signing.properties_sample deleted file mode 100644 index cf8e2396..00000000 --- a/signing.properties_sample +++ /dev/null @@ -1,3 +0,0 @@ -STORE_FILE=/path/to/your.keystore -KEY_ALIAS=projectkeyalias -PASSWORD=yourpass