diff options
-rw-r--r-- | CHANGELOG.md | 5 | ||||
-rw-r--r-- | app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt | 3 | ||||
-rw-r--r-- | app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt | 320 | ||||
-rw-r--r-- | app/src/main/res/values/strings.xml | 2 |
4 files changed, 177 insertions, 153 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index fc3c29dd3..4a888d738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,14 @@ Types of changes can be: Added/Changed/Deprecated/Removed/Fixed/Security ### Added ### Changed -- improve conversation list design wand dark/light theming +- improve conversation list design and dark/light theming ### Fixed - @ in username is allowed for phonebook sync - avoid sync when phonebook is empty +- avoid creation of multiple "chat via"-links in phonebook +- delete "chat via"-link from phonebook if phone number was deleted on server +- remove all "chat via"-links from phonebook when sync is disabled ## [11.1.0] - 2021-03-12 ### Added diff --git a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt index ce059de71..349f82d3c 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt @@ -176,7 +176,8 @@ class MainActivity : BaseActivity(), ActionBarProvider { if (userUtils.currentUser?.baseUrl?.endsWith(baseUrl) == true) { startConversation(user) } else { - Snackbar.make(container, "Account not found", Snackbar.LENGTH_LONG).show() + Snackbar.make(container, R.string.nc_phone_book_integration_account_not_found, Snackbar + .LENGTH_LONG).show() } } } diff --git a/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt index 833c8bd48..f015ab7ab 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt @@ -30,7 +30,6 @@ import android.content.pm.PackageManager import android.net.Uri import android.os.RemoteException import android.provider.ContactsContract -import android.provider.ContactsContract.CommonDataKinds.Phone.NUMBER import android.util.Log import androidx.core.content.ContextCompat import androidx.core.os.ConfigurationCompat @@ -99,15 +98,14 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar Log.d(TAG, "Account already exists") } - // collect all contacts with phone number - val contactsWithNumbers = collectPhoneNumbers() + val deviceContactsWithNumbers = collectContactsWithPhoneNumbersFromDevice() - if(contactsWithNumbers.isNotEmpty()){ + if(deviceContactsWithNumbers.isNotEmpty()){ val currentLocale = ConfigurationCompat.getLocales(context.resources.configuration)[0].country val map = mutableMapOf<String, Any>() map["location"] = currentLocale - map["search"] = contactsWithNumbers + map["search"] = deviceContactsWithNumbers val json = Gson().toJson(map) @@ -125,13 +123,14 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar } override fun onNext(foundContacts: ContactsByNumberOverall) { - up(foundContacts) + val contactsWithAssociatedPhoneNumbers = foundContacts.ocs.map + deleteLinkedAccounts(contactsWithAssociatedPhoneNumbers) + createLinkedAccounts(contactsWithAssociatedPhoneNumbers) } override fun onError(e: Throwable) { Log.e(javaClass.simpleName, "Failed to searchContactsByPhoneNumber", e) } - }) } @@ -141,8 +140,8 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar return Result.success() } - private fun collectPhoneNumbers(): MutableMap<String, List<String>> { - val result: MutableMap<String, List<String>> = mutableMapOf() + private fun collectContactsWithPhoneNumbersFromDevice(): MutableMap<String, List<String>> { + val deviceContactsWithNumbers: MutableMap<String, List<String>> = mutableMapOf() val contactCursor = context.contentResolver.query( ContactsContract.Contacts.CONTENT_URI, @@ -156,42 +155,32 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar if (contactCursor.count > 0) { contactCursor.moveToFirst() for (i in 0 until contactCursor.count) { - val numbers: MutableList<String> = mutableListOf() - val id = contactCursor.getString(contactCursor.getColumnIndex(ContactsContract.Contacts._ID)) val lookup = contactCursor.getString(contactCursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)) - - val phonesCursor = context.contentResolver.query( - ContactsContract.CommonDataKinds.Phone.CONTENT_URI, - null, - ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = " + id, - null, - null) - - if (phonesCursor != null) { - while (phonesCursor.moveToNext()) { - numbers.add(phonesCursor.getString(phonesCursor.getColumnIndex(NUMBER))) - } - - result[lookup] = numbers - - phonesCursor.close() - } - + deviceContactsWithNumbers[lookup] = getPhoneNumbersFromDeviceContact(id) contactCursor.moveToNext() } } - contactCursor.close() } - - return result + Log.d(TAG, "collected contacts with phonenumbers: " + deviceContactsWithNumbers.size) + return deviceContactsWithNumbers } - private fun up(foundContacts: ContactsByNumberOverall) { - val map = foundContacts.ocs.map - - // Delete all old associations (those that are associated on phone, but not in server response) + private fun deleteLinkedAccounts(contactsWithAssociatedPhoneNumbers: Map<String, String>?) { + Log.d(TAG, "deleteLinkedAccount") + fun deleteLinkedAccount(id: String) { + val rawContactUri = ContactsContract.RawContacts.CONTENT_URI + .buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) + .build() + val count = context.contentResolver.delete(rawContactUri, ContactsContract.RawContacts.CONTACT_ID + " " + + "LIKE \"" + id + "\"", null) + Log.d(TAG, "deleted $count linked accounts for id $id") + } + val rawContactUri = ContactsContract.Data.CONTENT_URI .buildUpon() .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") @@ -200,7 +189,6 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar .appendQueryParameter(ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat") .build() - // get all raw contacts val rawContactsCursor = context.contentResolver.query( rawContactUri, null, @@ -212,147 +200,177 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar if (rawContactsCursor != null) { if (rawContactsCursor.count > 0) { while (rawContactsCursor.moveToNext()) { - val id = rawContactsCursor.getString(rawContactsCursor.getColumnIndex(ContactsContract.RawContacts._ID)) - val sync1 = rawContactsCursor.getString(rawContactsCursor.getColumnIndex(ContactsContract.Data.SYNC1)) val lookupKey = rawContactsCursor.getString(rawContactsCursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY)) - Log.d("Contact", "Found associated: $id") + val contactId = rawContactsCursor.getString(rawContactsCursor.getColumnIndex(ContactsContract.Data.CONTACT_ID)) - if (map == null || !map.containsKey(lookupKey)) { - if (sync1 != null) { - deleteAssociation(sync1) - } + if (contactsWithAssociatedPhoneNumbers == null || !contactsWithAssociatedPhoneNumbers.containsKey(lookupKey)) { + deleteLinkedAccount(contactId) } } + } else { + Log.d(TAG, "no contacts with linked Talk Accounts found. Nothing to delete...") } - rawContactsCursor.close() } + } - // update / change found - if (map != null && map.isNotEmpty()) { - for (contact in foundContacts.ocs.map) { - val lookupKey = contact.key - val cloudId = contact.value - - update(lookupKey, cloudId) + private fun createLinkedAccounts(contactsWithAssociatedPhoneNumbers: Map<String, String>?) { + fun hasLinkedAccount(id: String) : Boolean { + var hasLinkedAccount = false + val where = ContactsContract.Data.MIMETYPE + " = ? AND " + ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID + " = ?" + val params = arrayOf(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, id) + + val rawContactUri = ContactsContract.Data.CONTENT_URI + .buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) + .appendQueryParameter(ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat") + .build() + + val rawContactsCursor = context.contentResolver.query( + rawContactUri, + null, + where, + params, + null + ) + + if (rawContactsCursor != null) { + if (rawContactsCursor.count > 0) { + hasLinkedAccount = true + Log.d(TAG, "contact with id $id already has a linked account") + } else { + hasLinkedAccount = false + } + rawContactsCursor.close() } + return hasLinkedAccount } - } - private fun update(uniqueId: String, cloudId: String) { - val lookupUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, uniqueId) - val lookupContactUri = ContactsContract.Contacts.lookupContact(context.contentResolver, lookupUri) - val contactCursor = context.contentResolver.query( - lookupContactUri, - null, - null, - null, - null) + fun createLinkedAccount(lookupKey: String, cloudId: String) { + val lookupUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, lookupKey) + val lookupContactUri = ContactsContract.Contacts.lookupContact(context.contentResolver, lookupUri) + val contactCursor = context.contentResolver.query( + lookupContactUri, + null, + null, + null, + null) - if (contactCursor != null) { - if (contactCursor.count > 0) { - contactCursor.moveToFirst() + if (contactCursor != null) { + if (contactCursor.count > 0) { + contactCursor.moveToFirst() - val id = contactCursor.getString(contactCursor.getColumnIndex(ContactsContract.Contacts._ID)) - - val phonesCursor = context.contentResolver.query( - ContactsContract.CommonDataKinds.Phone.CONTENT_URI, - null, - ContactsContract.Data.CONTACT_ID + " = " + id, - null, - null) - - val numbers = mutableListOf<String>() - if (phonesCursor != null) { - while (phonesCursor.moveToNext()) { - numbers.add(phonesCursor.getString(phonesCursor.getColumnIndex(NUMBER))) + val id = contactCursor.getString(contactCursor.getColumnIndex(ContactsContract.Contacts._ID)) + if(hasLinkedAccount(id)){ + return } - phonesCursor.close() - } + val numbers = getPhoneNumbersFromDeviceContact(id) + val displayName = getDisplayNameFromDeviceContact(id) - var displayName: String? = null - - val whereName = ContactsContract.Data.MIMETYPE + " = ? AND " + ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID + " = ?" - val whereNameParams = arrayOf(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, id) - val nameCursor = context.contentResolver.query( - ContactsContract.Data.CONTENT_URI, - null, - whereName, - whereNameParams, - ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME) - if (nameCursor != null) { - while (nameCursor.moveToNext()) { - displayName = nameCursor.getString(nameCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME)) + if (displayName == null) { + return } - nameCursor.close() - } - if (displayName == null) { - return - } + val ops = ArrayList<ContentProviderOperation>() + val rawContactsUri = ContactsContract.RawContacts.CONTENT_URI.buildUpon().build() + val dataUri = ContactsContract.Data.CONTENT_URI.buildUpon().build() + + ops.add(ContentProviderOperation + .newInsert(rawContactsUri) + .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) + .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) + .withValue(ContactsContract.RawContacts.AGGREGATION_MODE, + ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT) + .withValue(ContactsContract.RawContacts.SYNC2, cloudId) + .build()) + ops.add(ContentProviderOperation + .newInsert(dataUri) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, numbers[0]) + .build()) + ops.add(ContentProviderOperation + .newInsert(dataUri) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName) + .build()) + ops.add(ContentProviderOperation + .newInsert(dataUri) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat") + .withValue(ContactsContract.Data.DATA1, cloudId) + .withValue(ContactsContract.Data.DATA2, String.format(context.resources.getString(R + .string.nc_phone_book_integration_chat_via), accountName)) + .build()) + + try { + context.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops) + } catch (e: OperationApplicationException) { + Log.e(javaClass.simpleName, "", e) + } catch (e: RemoteException) { + Log.e(javaClass.simpleName, "", e) + } - // update entries - val ops = ArrayList<ContentProviderOperation>() - val rawContactsUri = ContactsContract.RawContacts.CONTENT_URI.buildUpon() - .build() - val dataUri = ContactsContract.Data.CONTENT_URI.buildUpon() - .build() - - ops.add(ContentProviderOperation - .newInsert(rawContactsUri) - .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) - .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) - .withValue(ContactsContract.RawContacts.AGGREGATION_MODE, - ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT) - .withValue(ContactsContract.RawContacts.SYNC2, cloudId) - .build()) - ops.add(ContentProviderOperation - .newInsert(dataUri) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) - .withValue(NUMBER, numbers[0]) - .build()) - ops.add(ContentProviderOperation - .newInsert(dataUri) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName) - .build()) - ops.add(ContentProviderOperation - .newInsert(dataUri) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat") - .withValue(ContactsContract.Data.DATA1, cloudId) - .withValue(ContactsContract.Data.DATA2, "Chat via " + context.resources.getString(R.string.nc_app_name)) - .build()) - - try { - context.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops) - } catch (e: OperationApplicationException) { - Log.e(javaClass.simpleName, "", e) - } catch (e: RemoteException) { - Log.e(javaClass.simpleName, "", e) + Log.d(TAG, "added new entry for contact $displayName (cloudId: $cloudId | lookupKey: $lookupKey" + + " | id: $id)") } + contactCursor.close() } + } - contactCursor.close() + if (contactsWithAssociatedPhoneNumbers != null && contactsWithAssociatedPhoneNumbers.isNotEmpty()) { + for (contact in contactsWithAssociatedPhoneNumbers) { + val lookupKey = contact.key + val cloudId = contact.value + createLinkedAccount(lookupKey, cloudId) + } + } else { + Log.d(TAG, "no contacts with linked Talk Accounts found. No linked accounts created.") } } - private fun deleteAssociation(id: String) { - Log.d("Contact", "Delete associated: $id") - - val rawContactUri = ContactsContract.RawContacts.CONTENT_URI - .buildUpon() - .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") - .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) - .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) - .build() + private fun getDisplayNameFromDeviceContact(id: String?): String? { + var displayName:String? = null + val whereName = ContactsContract.Data.MIMETYPE + " = ? AND " + ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID + " = ?" + val whereNameParams = arrayOf(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, id) + val nameCursor = context.contentResolver.query( + ContactsContract.Data.CONTENT_URI, + null, + whereName, + whereNameParams, + ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME) + if (nameCursor != null) { + while (nameCursor.moveToNext()) { + displayName = nameCursor.getString(nameCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME)) + } + nameCursor.close() + } + return displayName + } + private fun getPhoneNumbersFromDeviceContact(id: String?): MutableList<String> { + val numbers = mutableListOf<String>() + val phonesNumbersCursor = context.contentResolver.query( + ContactsContract.CommonDataKinds.Phone.CONTENT_URI, + null, + ContactsContract.Data.CONTACT_ID + " = " + id, + null, + null) - val count = context.contentResolver.delete(rawContactUri, ContactsContract.RawContacts.SYNC2 + " LIKE \"" + id + "\"", null) - Log.d("Contact", "deleted $count for id $id") + if (phonesNumbersCursor != null) { + while (phonesNumbersCursor.moveToNext()) { + numbers.add(phonesNumbersCursor.getString(phonesNumbersCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))) + } + phonesNumbersCursor.close() + } + if(numbers.size > 0){ + Log.d(TAG, "Found ${numbers.size} phone numbers for contact with id $id") + } + return numbers } companion object { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 68066ecfa..97d8f9b49 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -354,6 +354,8 @@ <string name="nc_settings_phone_book_integration_phone_number_dialog_invalid">Invalid phone number</string> <string name="nc_settings_phone_book_integration_phone_number_dialog_success">Phone number set successfully</string> <string name="no_phone_book_integration_due_to_permissions">No phone book integration due to missing permissions</string> + <string name="nc_phone_book_integration_chat_via">Chat via %s</string> + <string name="nc_phone_book_integration_account_not_found">Account not found</string> <!-- Non-translatable strings --> <string name="path_password_strike_through" translatable="false" |