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