How to retrieve last edited contact from ContactsProvider
Asked Answered
F

2

0

I have to display new contact added or contact edited. I am able to get newly added contact but, I am not getting last edited contact. I tried to retrieve edited contact on the basis of CONTACT_LAST_UPDATED_TIMESTAMP, but if we are making any call then CONTACT_LAST_UPDATED_TIMESTAMP of called contact is getting modified in ContactsProvider and thus its returning me last called contact as last edited contact. I have written query as given below:

Cursor cursor = context.getContentResolver().query(uri, null,
                null,
                null,
                ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " DESC LIMIT 1");
Fandango answered 30/8, 2017 at 8:16 Comment(0)
D
1

You can monitor the changes of a specific contact by using registerContentObserver(ContactsContract.Contacts.CONTENT_URI...).

Then you check the value of CONTACT_LAST_UPDATED_TIMESTAMP, and compare to previous one.

When it's different, it's time to compare new fields and old fields (or initialize if there are no old ones.

I've prepared a ViewModel that can help with the monitoring. You can observe its contactStateLiveData, and when it changes, it's time to query about the contact's fields. You can get more fields prepared for you right in the viewModel itself inside contactStateLiveData, or you can query in a different way each time it changes.

I've also made it change every up-to 500 ms, because it sometimes gets called a lot.

class ABContactDetailsFragmentViewModel(application: Application) : BaseViewModel(application) {
    private val updateContactDebounceJob = DebounceJob()
    private var contactKeyToObserver: Pair<Uri, ContentObserver>? = null
    val contactStateLiveData = MutableLiveData<ContactState>(ContactState.Unknown)

    sealed class ContactState {
        data object Unknown : ContactState()
        class Exists(val lastUpdated: Long) : ContactState()
        data object Removed : ContactState()
    }

    /**monitor for changes of the specific contactKey of the contact.
     * Note that you should also call [checkUpdatesOfContactIfNeeded] after that, preferably on onResume as it might get the callback a bit late*/
    @RequiresPermission(permission.READ_CONTACTS)
    @UiThread
    fun monitorContact(contactKey: String) {
        val contentResolver = applicationContext.contentResolver
        val lookupUri: Uri = ContactsContract.Contacts.getLookupUri(contentResolver, Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, contactKey))
        monitorContact(lookupUri)
    }

    /**monitor for changes of the specific uri of the contact.
     * Note that you should also call [checkUpdatesOfContactIfNeeded] after that, preferably on onResume as it might get the callback a bit late*/
    @RequiresPermission(permission.READ_CONTACTS)
    @UiThread
    fun monitorContact(lookupUri: Uri) {
        val contentResolver = applicationContext.contentResolver
        contactKeyToObserver?.let { contactKeyToObserver ->
            if (contactKeyToObserver.first == lookupUri) return
            applicationContext.contentResolver.unregisterContentObserver(contactKeyToObserver.second)
            [email protected] = null
            contactStateLiveData.value = ContactState.Unknown
        }

        val observer = object : ContentObserver(Executors.uiHandler) {
            override fun onChange(selfChange: Boolean) {
                super.onChange(selfChange)
                checkUpdatesOfContactIfNeeded()
            }
        }
        contentResolver.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, false, observer)
        onClearedListeners.add {
            contentResolver.unregisterContentObserver(observer)
        }
        this.contactKeyToObserver = Pair(lookupUri, observer)
    }

    @UiThread
    fun checkUpdatesOfContactIfNeeded() {
//        Log.d("AppLog", "ABContactDetailsFragmentViewModel checkUpdatesOfContactIfNeeded")
        val lookupUri = contactKeyToObserver?.first ?: return
        updateContactDebounceJob.debounce(scope = viewModelScope, runnable = {
            viewModelScope.launch {
                runInterruptible(Dispatchers.IO) {
//                    Log.d("AppLog", "ABContactDetailsFragmentViewModel scan for changes of monitored contact in background thread")
                    checkIfContactUpdated(lookupUri)
                }
            }
        })
    }

    @WorkerThread
    private fun checkIfContactUpdated(lookupUri: Uri) {
//        Log.d("AppLog", "ABContactDetailsFragmentViewModel checkIfContactUpdated lookupUri:$lookupUri")
        val state = runOnUiThreadWithResult {
            contactStateLiveData.value
        }
        val lastUpdatedTimeStampMs: Long? = if (state is ContactState.Exists) {
            state.lastUpdated
        } else null

        val projection: Array<out String> = arrayOf(
//                        ContactsContract.Contacts._ID,
//                ContactsContract.Contacts.LOOKUP_KEY,
//                ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
                ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
        )
        val cursor = applicationContext.contentResolver.query(lookupUri, projection, null, null, null)
        if (cursor == null || cursor.count == 0) {
//            Log.d("AppLog", "ABContactDetailsFragmentViewModel contact not found")
            cursor?.closeQuietly()
            contactStateLiveData.postValue(ContactState.Removed)
            return
        }
        cursor.use {
            cursor.moveToNext()
            val timestampIdx = cursor.getColumnIndex(ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP)
            val timestampMs = cursor.getLong(timestampIdx)
            val detectedChange = lastUpdatedTimeStampMs != timestampMs
//            Log.d("AppLog", "ABContactDetailsFragmentViewModel detected change ? $detectedChange $lastUpdatedTimeStampMs->$timestampMs ${DatabaseUtils.dumpCurrentRowToString(cursor)} ")
            if (detectedChange) {
                contactStateLiveData.postValue(ContactState.Exists(timestampMs))
            }
        }
    }
}
//https://mcmap.net/q/265527/-kotlin-android-debounce
class DebounceJob(private val defaultDebounceDelayMs:Long=500L) {
    private var job: Job? = null

    @UiThread
    fun debounce(delayMs: Long = defaultDebounceDelayMs, scope: CoroutineScope, runnable: Runnable) {
        job?.cancel()
        job = scope.launch {
            delay(delayMs)
            runnable.run()
        }
    }
}
fun interface ResultCallback<T> {
    fun getResult(): T
}

fun <T> runOnUiThreadWithResult(callback: ResultCallback<T>): T {
    if (isUiThread())
        return callback.getResult()
    val countDownLatch = CountDownLatch(1)
    val resultRef = AtomicReference<T>()
    Executors.uiHandler.post {
        resultRef.set(callback.getResult())
        countDownLatch.countDown()
    }
    countDownLatch.await()
    return resultRef.get()
}
Discipline answered 30/4 at 12:26 Comment(0)
K
0

Instead of ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP, you should use ContactsContract.Contacts._ID like

Cursor cursor = context.getContentResolver().query(uri, null,
                null,
                null,
                ContactsContract.Contacts._ID + " DESC LIMIT 1");
Kernel answered 30/8, 2017 at 8:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.