fix "chat via"-links in phonebook (no multiple entries, fix deletion)

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2021-03-25 08:15:02 +01:00
parent 711afaccf5
commit adc0a91dac
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
4 changed files with 188 additions and 164 deletions

View File

@ -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

View File

@ -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()
}
}
}

View File

@ -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
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")
}
// Delete all old associations (those that are associated on phone, but not in server response)
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,35 +200,56 @@ 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 update(uniqueId: String, cloudId: String) {
val lookupUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, uniqueId)
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
}
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,
@ -254,25 +263,78 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
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)))
if(hasLinkedAccount(id)){
return
}
phonesCursor.close()
val numbers = getPhoneNumbersFromDeviceContact(id)
val displayName = getDisplayNameFromDeviceContact(id)
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)
}
Log.d(TAG, "added new entry for contact $displayName (cloudId: $cloudId | lookupKey: $lookupKey" +
" | id: $id)")
}
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 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(
@ -287,72 +349,28 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
}
nameCursor.close()
}
if (displayName == null) {
return
return displayName
}
// update entries
val ops = ArrayList<ContentProviderOperation>()
val rawContactsUri = ContactsContract.RawContacts.CONTENT_URI.buildUpon()
.build()
val dataUri = ContactsContract.Data.CONTENT_URI.buildUpon()
.build()
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)
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)
if (phonesNumbersCursor != null) {
while (phonesNumbersCursor.moveToNext()) {
numbers.add(phonesNumbersCursor.getString(phonesNumbersCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)))
}
phonesNumbersCursor.close()
}
contactCursor.close()
if(numbers.size > 0){
Log.d(TAG, "Found ${numbers.size} phone numbers for contact with id $id")
}
}
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()
val count = context.contentResolver.delete(rawContactUri, ContactsContract.RawContacts.SYNC2 + " LIKE \"" + id + "\"", null)
Log.d("Contact", "deleted $count for id $id")
return numbers
}
companion object {

View File

@ -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"