From 7e9cf088723dbfcb8fade161a1dd0b5621f098f4 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Wed, 24 Jun 2026 16:10:06 +0530 Subject: [PATCH 1/3] feat: implement ContentProvider-based external control system for app features and settings --- app/src/main/AndroidManifest.xml | 19 +++++ .../external/ExternalActionReceiver.kt | 31 ++++++++ .../external/ExternalControlProvider.kt | 70 +++++++++++++++++++ .../essentials/external/ExternalHandler.kt | 12 ++++ .../essentials/external/ExternalRouter.kt | 44 ++++++++++++ .../external/SettingsExternalHandler.kt | 66 +++++++++++++++++ app/src/main/res/values/strings.xml | 3 + 7 files changed, 245 insertions(+) create mode 100644 app/src/main/java/com/sameerasw/essentials/external/ExternalActionReceiver.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/external/ExternalControlProvider.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/external/ExternalHandler.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/external/ExternalRouter.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/external/SettingsExternalHandler.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e55504e44..e64c0c945 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,6 +36,10 @@ + @@ -909,6 +913,21 @@ android:value="android.service.quicksettings.CATEGORY_DISPLAY" /> + + + + + + + + diff --git a/app/src/main/java/com/sameerasw/essentials/external/ExternalActionReceiver.kt b/app/src/main/java/com/sameerasw/essentials/external/ExternalActionReceiver.kt new file mode 100644 index 000000000..173d20929 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/external/ExternalActionReceiver.kt @@ -0,0 +1,31 @@ +package com.sameerasw.essentials.external + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log + +class ExternalActionReceiver : BroadcastReceiver() { + companion object { + const val ACTION_EXTERNAL_CONTROL = "com.sameerasw.essentials.action.EXTERNAL_CONTROL" + const val EXTRA_PATH = "path" + const val EXTRA_ACTION = "action" + const val EXTRA_VALUE = "value" + } + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != ACTION_EXTERNAL_CONTROL) return + + val path = intent.getStringExtra(EXTRA_PATH) ?: return + val action = intent.getStringExtra(EXTRA_ACTION) + val value = intent.getStringExtra(EXTRA_VALUE) + + Log.d("ExternalActionReceiver", "Received external control request: path=$path, action=$action, value=$value") + + if (action == "update") { + ExternalRouter.update(context, path, value, intent.extras) + } else if (action != null) { + ExternalRouter.action(context, path, action, intent.extras) + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/external/ExternalControlProvider.kt b/app/src/main/java/com/sameerasw/essentials/external/ExternalControlProvider.kt new file mode 100644 index 000000000..7038c9705 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/external/ExternalControlProvider.kt @@ -0,0 +1,70 @@ +package com.sameerasw.essentials.external + +import android.content.ContentProvider +import android.content.ContentValues +import android.database.Cursor +import android.database.MatrixCursor +import android.net.Uri +import android.os.Bundle + +class ExternalControlProvider : ContentProvider() { + + override fun onCreate(): Boolean { + return true + } + + @Suppress("DEPRECATION") + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? { + val context = context ?: return null + val path = uri.path ?: return null + val result = ExternalRouter.query(context, path, null) ?: return null + + val cursor = MatrixCursor(arrayOf("key", "value", "type")) + val key = path.substringAfterLast('/') + val value = result.get("value") + val type = result.getString("type", "") + cursor.addRow(arrayOf(key, value, type)) + return cursor + } + + override fun getType(uri: Uri): String? { + return "vnd.android.cursor.item/vnd.com.sameerasw.essentials.external" + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + return null + } + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { + return 0 + } + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int { + val context = context ?: return 0 + val path = uri.path ?: return 0 + val value = values?.getAsString("value") ?: return 0 + val success = ExternalRouter.update(context, path, value, null) + return if (success) 1 else 0 + } + + override fun call(method: String, arg: String?, extras: Bundle?): Bundle? { + val context = context ?: return null + if (method == "action") { + val path = arg ?: return null + val action = extras?.getString("action") + return ExternalRouter.action(context, path, action, extras) + } + return null + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/external/ExternalHandler.kt b/app/src/main/java/com/sameerasw/essentials/external/ExternalHandler.kt new file mode 100644 index 000000000..5b8f22f71 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/external/ExternalHandler.kt @@ -0,0 +1,12 @@ +package com.sameerasw.essentials.external + +import android.content.Context +import android.os.Bundle + +interface ExternalHandler { + val path: String + + fun onQuery(context: Context, remainingPath: String, extras: Bundle?): Bundle? + fun onUpdate(context: Context, remainingPath: String, value: String?, extras: Bundle?): Boolean + fun onAction(context: Context, remainingPath: String, action: String?, extras: Bundle?): Bundle? +} diff --git a/app/src/main/java/com/sameerasw/essentials/external/ExternalRouter.kt b/app/src/main/java/com/sameerasw/essentials/external/ExternalRouter.kt new file mode 100644 index 000000000..45f2c7c24 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/external/ExternalRouter.kt @@ -0,0 +1,44 @@ +package com.sameerasw.essentials.external + +import android.content.Context +import android.os.Bundle + +object ExternalRouter { + private val handlers = mutableMapOf() + + init { + registerHandler(SettingsExternalHandler()) + } + + fun registerHandler(handler: ExternalHandler) { + handlers[handler.path] = handler + } + + private fun getHandlerAndRemainingPath(fullPath: String): Pair? { + val cleanPath = fullPath.trim('/') + for ((registeredPath, handler) in handlers) { + if (cleanPath == registeredPath) { + return Pair(handler, "") + } else if (cleanPath.startsWith("$registeredPath/")) { + val remaining = cleanPath.substring(registeredPath.length + 1) + return Pair(handler, remaining) + } + } + return null + } + + fun query(context: Context, path: String, extras: Bundle?): Bundle? { + val (handler, remaining) = getHandlerAndRemainingPath(path) ?: return null + return handler.onQuery(context, remaining, extras) + } + + fun update(context: Context, path: String, value: String?, extras: Bundle?): Boolean { + val (handler, remaining) = getHandlerAndRemainingPath(path) ?: return false + return handler.onUpdate(context, remaining, value, extras) + } + + fun action(context: Context, path: String, action: String?, extras: Bundle?): Bundle? { + val (handler, remaining) = getHandlerAndRemainingPath(path) ?: return null + return handler.onAction(context, remaining, action, extras) + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/external/SettingsExternalHandler.kt b/app/src/main/java/com/sameerasw/essentials/external/SettingsExternalHandler.kt new file mode 100644 index 000000000..934e5d357 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/external/SettingsExternalHandler.kt @@ -0,0 +1,66 @@ +package com.sameerasw.essentials.external + +import android.content.Context +import android.os.Bundle +import com.sameerasw.essentials.data.repository.SettingsRepository + +class SettingsExternalHandler : ExternalHandler { + override val path: String = "settings" + + override fun onQuery(context: Context, remainingPath: String, extras: Bundle?): Bundle? { + val key = remainingPath + val prefs = context.getSharedPreferences(SettingsRepository.PREFS_NAME, Context.MODE_PRIVATE) + if (!prefs.contains(key)) return null + + val value = prefs.all[key] ?: return null + val bundle = Bundle() + when (value) { + is Boolean -> bundle.putBoolean("value", value) + is String -> bundle.putString("value", value) + is Int -> bundle.putInt("value", value) + is Float -> bundle.putFloat("value", value) + is Long -> bundle.putLong("value", value) + else -> bundle.putString("value", value.toString()) + } + bundle.putString("type", value.javaClass.simpleName) + return bundle + } + + override fun onUpdate(context: Context, remainingPath: String, value: String?, extras: Bundle?): Boolean { + val key = remainingPath + val prefs = context.getSharedPreferences(SettingsRepository.PREFS_NAME, Context.MODE_PRIVATE) + if (!prefs.contains(key)) return false + + val currentValue = prefs.all[key] ?: return false + val repository = SettingsRepository(context) + + return try { + when (currentValue) { + is Boolean -> repository.putBoolean(key, value?.toBoolean() ?: false) + is String -> repository.putString(key, value) + is Int -> repository.putInt(key, value?.toInt() ?: 0) + is Float -> repository.putFloat(key, value?.toFloat() ?: 0f) + is Long -> repository.putLong(key, value?.toLong() ?: 0L) + else -> false + } + true + } catch (e: Exception) { + false + } + } + + override fun onAction(context: Context, remainingPath: String, action: String?, extras: Bundle?): Bundle? { + if (action == "toggle") { + val key = remainingPath + val prefs = context.getSharedPreferences(SettingsRepository.PREFS_NAME, Context.MODE_PRIVATE) + val currentValue = prefs.all[key] + if (currentValue is Boolean) { + val repository = SettingsRepository(context) + val newValue = !currentValue + repository.putBoolean(key, newValue) + return Bundle().apply { putBoolean("value", newValue) } + } + } + return null + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7fe5b43d0..6aa20b680 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1556,4 +1556,7 @@ Favorite Features View and launch your favorite features at a glance. No favorite features pinned yet. Open the app to pin some! + + Control Essentials features + Allows the app to read, update and control Essentials features and settings. From c77a4c0b05dc9d176345846220f4133de687941c Mon Sep 17 00:00:00 2001 From: sameerasw Date: Wed, 24 Jun 2026 16:24:58 +0530 Subject: [PATCH 2/3] feat: migrate external query interface to use Cursor and add LocationAlarmExternalHandler support --- app/src/main/AndroidManifest.xml | 11 ++- .../external/ExternalControlProvider.kt | 10 +-- .../essentials/external/ExternalHandler.kt | 3 +- .../essentials/external/ExternalRouter.kt | 4 +- .../external/LocationAlarmExternalHandler.kt | 73 +++++++++++++++++++ .../external/SettingsExternalHandler.kt | 18 ++--- app/src/main/res/values/strings.xml | 4 +- 7 files changed, 97 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/com/sameerasw/essentials/external/LocationAlarmExternalHandler.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e64c0c945..ab215e52d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,10 +36,19 @@ + + + + android:description="@string/perm_external_control_desc" + android:icon="@drawable/app_logo" /> diff --git a/app/src/main/java/com/sameerasw/essentials/external/ExternalControlProvider.kt b/app/src/main/java/com/sameerasw/essentials/external/ExternalControlProvider.kt index 7038c9705..8d0669e12 100644 --- a/app/src/main/java/com/sameerasw/essentials/external/ExternalControlProvider.kt +++ b/app/src/main/java/com/sameerasw/essentials/external/ExternalControlProvider.kt @@ -13,7 +13,6 @@ class ExternalControlProvider : ContentProvider() { return true } - @Suppress("DEPRECATION") override fun query( uri: Uri, projection: Array?, @@ -23,14 +22,7 @@ class ExternalControlProvider : ContentProvider() { ): Cursor? { val context = context ?: return null val path = uri.path ?: return null - val result = ExternalRouter.query(context, path, null) ?: return null - - val cursor = MatrixCursor(arrayOf("key", "value", "type")) - val key = path.substringAfterLast('/') - val value = result.get("value") - val type = result.getString("type", "") - cursor.addRow(arrayOf(key, value, type)) - return cursor + return ExternalRouter.query(context, path, null) } override fun getType(uri: Uri): String? { diff --git a/app/src/main/java/com/sameerasw/essentials/external/ExternalHandler.kt b/app/src/main/java/com/sameerasw/essentials/external/ExternalHandler.kt index 5b8f22f71..0b15fdd49 100644 --- a/app/src/main/java/com/sameerasw/essentials/external/ExternalHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/external/ExternalHandler.kt @@ -1,12 +1,13 @@ package com.sameerasw.essentials.external import android.content.Context +import android.database.Cursor import android.os.Bundle interface ExternalHandler { val path: String - fun onQuery(context: Context, remainingPath: String, extras: Bundle?): Bundle? + fun onQuery(context: Context, remainingPath: String, extras: Bundle?): Cursor? fun onUpdate(context: Context, remainingPath: String, value: String?, extras: Bundle?): Boolean fun onAction(context: Context, remainingPath: String, action: String?, extras: Bundle?): Bundle? } diff --git a/app/src/main/java/com/sameerasw/essentials/external/ExternalRouter.kt b/app/src/main/java/com/sameerasw/essentials/external/ExternalRouter.kt index 45f2c7c24..6ca92b900 100644 --- a/app/src/main/java/com/sameerasw/essentials/external/ExternalRouter.kt +++ b/app/src/main/java/com/sameerasw/essentials/external/ExternalRouter.kt @@ -1,6 +1,7 @@ package com.sameerasw.essentials.external import android.content.Context +import android.database.Cursor import android.os.Bundle object ExternalRouter { @@ -8,6 +9,7 @@ object ExternalRouter { init { registerHandler(SettingsExternalHandler()) + registerHandler(LocationAlarmExternalHandler()) } fun registerHandler(handler: ExternalHandler) { @@ -27,7 +29,7 @@ object ExternalRouter { return null } - fun query(context: Context, path: String, extras: Bundle?): Bundle? { + fun query(context: Context, path: String, extras: Bundle?): Cursor? { val (handler, remaining) = getHandlerAndRemainingPath(path) ?: return null return handler.onQuery(context, remaining, extras) } diff --git a/app/src/main/java/com/sameerasw/essentials/external/LocationAlarmExternalHandler.kt b/app/src/main/java/com/sameerasw/essentials/external/LocationAlarmExternalHandler.kt new file mode 100644 index 000000000..f174873a8 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/external/LocationAlarmExternalHandler.kt @@ -0,0 +1,73 @@ +package com.sameerasw.essentials.external + +import android.content.Context +import android.content.Intent +import android.database.Cursor +import android.database.MatrixCursor +import android.os.Bundle +import com.sameerasw.essentials.data.repository.LocationReachedRepository +import com.sameerasw.essentials.services.LocationReachedService + +class LocationAlarmExternalHandler : ExternalHandler { + override val path: String = "location_alarm" + + override fun onQuery(context: Context, remainingPath: String, extras: Bundle?): Cursor? { + val repository = LocationReachedRepository(context) + if (remainingPath == "list") { + val alarms = repository.getAlarms() + val activeId = repository.getActiveAlarmId() + + val cursor = MatrixCursor(arrayOf( + "id", + "name", + "latitude", + "longitude", + "radius", + "isEnabled", + "isPaused", + "lastTravelled", + "isActive" + )) + + for (alarm in alarms) { + cursor.addRow(arrayOf( + alarm.id, + alarm.name, + alarm.latitude, + alarm.longitude, + alarm.radius, + if (alarm.isEnabled) 1 else 0, + if (alarm.isPaused) 1 else 0, + alarm.lastTravelled ?: 0L, + if (alarm.id == activeId) 1 else 0 + )) + } + return cursor + } + return null + } + + override fun onUpdate(context: Context, remainingPath: String, value: String?, extras: Bundle?): Boolean { + return false + } + + override fun onAction(context: Context, remainingPath: String, action: String?, extras: Bundle?): Bundle? { + val repository = LocationReachedRepository(context) + when (action) { + "start" -> { + val alarmId = extras?.getString("id") ?: return null + repository.saveActiveAlarmId(alarmId) + LocationReachedService.start(context) + return Bundle().apply { putBoolean("success", true) } + } + "stop" -> { + val intent = Intent(context, LocationReachedService::class.java).apply { + this.action = LocationReachedService.ACTION_STOP + } + context.startService(intent) + return Bundle().apply { putBoolean("success", true) } + } + } + return null + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/external/SettingsExternalHandler.kt b/app/src/main/java/com/sameerasw/essentials/external/SettingsExternalHandler.kt index 934e5d357..c158d9c36 100644 --- a/app/src/main/java/com/sameerasw/essentials/external/SettingsExternalHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/external/SettingsExternalHandler.kt @@ -1,29 +1,23 @@ package com.sameerasw.essentials.external import android.content.Context +import android.database.Cursor +import android.database.MatrixCursor import android.os.Bundle import com.sameerasw.essentials.data.repository.SettingsRepository class SettingsExternalHandler : ExternalHandler { override val path: String = "settings" - override fun onQuery(context: Context, remainingPath: String, extras: Bundle?): Bundle? { + override fun onQuery(context: Context, remainingPath: String, extras: Bundle?): Cursor? { val key = remainingPath val prefs = context.getSharedPreferences(SettingsRepository.PREFS_NAME, Context.MODE_PRIVATE) if (!prefs.contains(key)) return null val value = prefs.all[key] ?: return null - val bundle = Bundle() - when (value) { - is Boolean -> bundle.putBoolean("value", value) - is String -> bundle.putString("value", value) - is Int -> bundle.putInt("value", value) - is Float -> bundle.putFloat("value", value) - is Long -> bundle.putLong("value", value) - else -> bundle.putString("value", value.toString()) - } - bundle.putString("type", value.javaClass.simpleName) - return bundle + val cursor = MatrixCursor(arrayOf("key", "value", "type")) + cursor.addRow(arrayOf(key, value, value.javaClass.simpleName)) + return cursor } override fun onUpdate(context: Context, remainingPath: String, value: String?, extras: Bundle?): Boolean { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6aa20b680..0e1a042f5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1557,6 +1557,6 @@ View and launch your favorite features at a glance. No favorite features pinned yet. Open the app to pin some! - Control Essentials features - Allows the app to read, update and control Essentials features and settings. + control Essentials features + read, update and control Essentials features and settings From e8fca55c5b3615346b653e6b59ff26360c842416 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Wed, 24 Jun 2026 16:35:42 +0530 Subject: [PATCH 3/3] feat: add iconResName to alarm projection and update onAction logic to support path-based routing and stop actions --- .../external/LocationAlarmExternalHandler.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/external/LocationAlarmExternalHandler.kt b/app/src/main/java/com/sameerasw/essentials/external/LocationAlarmExternalHandler.kt index f174873a8..ed572d1d2 100644 --- a/app/src/main/java/com/sameerasw/essentials/external/LocationAlarmExternalHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/external/LocationAlarmExternalHandler.kt @@ -26,7 +26,8 @@ class LocationAlarmExternalHandler : ExternalHandler { "isEnabled", "isPaused", "lastTravelled", - "isActive" + "isActive", + "iconResName" )) for (alarm in alarms) { @@ -39,7 +40,8 @@ class LocationAlarmExternalHandler : ExternalHandler { if (alarm.isEnabled) 1 else 0, if (alarm.isPaused) 1 else 0, alarm.lastTravelled ?: 0L, - if (alarm.id == activeId) 1 else 0 + if (alarm.id == activeId) 1 else 0, + alarm.iconResName )) } return cursor @@ -53,7 +55,8 @@ class LocationAlarmExternalHandler : ExternalHandler { override fun onAction(context: Context, remainingPath: String, action: String?, extras: Bundle?): Bundle? { val repository = LocationReachedRepository(context) - when (action) { + val targetAction = action ?: remainingPath + when (targetAction) { "start" -> { val alarmId = extras?.getString("id") ?: return null repository.saveActiveAlarmId(alarmId) @@ -61,6 +64,7 @@ class LocationAlarmExternalHandler : ExternalHandler { return Bundle().apply { putBoolean("success", true) } } "stop" -> { + repository.saveActiveAlarmId(null) val intent = Intent(context, LocationReachedService::class.java).apply { this.action = LocationReachedService.ACTION_STOP }