From 98948ac9b6e96b1e3c7d247460ece8e4ab4a5a66 Mon Sep 17 00:00:00 2001 From: dsremo Date: Wed, 13 May 2026 14:41:41 +0530 Subject: [PATCH 1/2] MainActivity: auto-refresh contact list on system contact changes When a contact is added/edited/deleted by another app (e.g., system contact picker, sync adapter, contact-import tools), Fossify Contacts currently only re-reads the contact list on next onResume(). If the user is already on the contact list when the change happens, the stale list stays on screen until they navigate away and back. This registers a ContentObserver on ContactsContract.Contacts.CONTENT_URI in onResume() and unregisters in onPause(). When the observer fires, refreshContacts(ALL_TABS_MASK) is debounced by 500ms (to coalesce multi-row bulk operations) and executed on the main thread handler. Behaviour outside of registered visibility (i.e., when the activity is paused/destroyed) is unchanged - no work happens off-screen. The observer runs only while the activity is visible. --- .../contacts/activities/MainActivity.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/app/src/main/kotlin/org/fossify/contacts/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/contacts/activities/MainActivity.kt index d26b748f5..302e0ccc7 100644 --- a/app/src/main/kotlin/org/fossify/contacts/activities/MainActivity.kt +++ b/app/src/main/kotlin/org/fossify/contacts/activities/MainActivity.kt @@ -4,10 +4,14 @@ import android.annotation.SuppressLint import android.content.ActivityNotFoundException import android.content.Intent import android.content.pm.ShortcutInfo +import android.database.ContentObserver import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Icon import android.graphics.drawable.LayerDrawable import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.provider.ContactsContract import androidx.viewpager.widget.ViewPager import me.grantland.widget.AutofitHelper import org.fossify.commons.databases.ContactsDatabase @@ -143,12 +147,14 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { isFirstResume = false checkShortcuts() + registerContactObserver() } override fun onPause() { super.onPause() storeStateVariables() config.lastUsedViewPagerPage = binding.viewPager.currentItem + unregisterContactObserver() } override fun onDestroy() { @@ -158,6 +164,38 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { } } + private val contactObserverHandler = Handler(Looper.getMainLooper()) + private val contactReloadRunnable = Runnable { + if (werePermissionsHandled) { + refreshContacts(ALL_TABS_MASK) + } + } + private var contactObserver: ContentObserver? = null + + private fun registerContactObserver() { + unregisterContactObserver() + val observer = object : ContentObserver(contactObserverHandler) { + override fun onChange(selfChange: Boolean) { + contactObserverHandler.removeCallbacks(contactReloadRunnable) + contactObserverHandler.postDelayed(contactReloadRunnable, 500) + } + } + runCatching { + contentResolver.registerContentObserver( + ContactsContract.Contacts.CONTENT_URI, true, observer + ) + contactObserver = observer + } + } + + private fun unregisterContactObserver() { + contactObserverHandler.removeCallbacks(contactReloadRunnable) + contactObserver?.let { observer -> + runCatching { contentResolver.unregisterContentObserver(observer) } + } + contactObserver = null + } + override fun onBackPressedCompat(): Boolean { return if (binding.mainMenu.isSearchOpen) { binding.mainMenu.closeSearch() From e977913e3420127b90fcded74e093105c75b0523 Mon Sep 17 00:00:00 2001 From: dsremo Date: Fri, 29 May 2026 14:02:15 +0530 Subject: [PATCH 2/2] MainActivity: extract debounce delay to named constant to satisfy detekt MagicNumber The 500ms `postDelayed` literal tripped detekt's MagicNumber rule (project allowlist is -1, 0, 1, 2, 42, 1000). Move it to a companion `const val CONTACT_REFRESH_DEBOUNCE_MS` so it's ignored by `ignoreCompanionObjectPropertyDeclaration` (default true) and gives the value a self-describing name for future maintainers. No behavior change. Same 500ms debounce window for ContactsContract observer events. --- .../kotlin/org/fossify/contacts/activities/MainActivity.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/org/fossify/contacts/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/contacts/activities/MainActivity.kt index 302e0ccc7..229487462 100644 --- a/app/src/main/kotlin/org/fossify/contacts/activities/MainActivity.kt +++ b/app/src/main/kotlin/org/fossify/contacts/activities/MainActivity.kt @@ -41,6 +41,10 @@ import org.fossify.contacts.interfaces.RefreshContactsListener import java.util.Arrays class MainActivity : SimpleActivity(), RefreshContactsListener { + companion object { + private const val CONTACT_REFRESH_DEBOUNCE_MS = 500L + } + private var werePermissionsHandled = false private var isFirstResume = true private var isGettingContacts = false @@ -177,7 +181,7 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { val observer = object : ContentObserver(contactObserverHandler) { override fun onChange(selfChange: Boolean) { contactObserverHandler.removeCallbacks(contactReloadRunnable) - contactObserverHandler.postDelayed(contactReloadRunnable, 500) + contactObserverHandler.postDelayed(contactReloadRunnable, CONTACT_REFRESH_DEBOUNCE_MS) } } runCatching {