diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e55504e44..ab215e52d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -36,6 +36,19 @@
+
+
+
+
@@ -909,6 +922,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..8d0669e12
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/external/ExternalControlProvider.kt
@@ -0,0 +1,62 @@
+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
+ }
+
+ 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
+ return ExternalRouter.query(context, path, null)
+ }
+
+ 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..0b15fdd49
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/external/ExternalHandler.kt
@@ -0,0 +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?): 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
new file mode 100644
index 000000000..6ca92b900
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/external/ExternalRouter.kt
@@ -0,0 +1,46 @@
+package com.sameerasw.essentials.external
+
+import android.content.Context
+import android.database.Cursor
+import android.os.Bundle
+
+object ExternalRouter {
+ private val handlers = mutableMapOf()
+
+ init {
+ registerHandler(SettingsExternalHandler())
+ registerHandler(LocationAlarmExternalHandler())
+ }
+
+ 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?): Cursor? {
+ 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/LocationAlarmExternalHandler.kt b/app/src/main/java/com/sameerasw/essentials/external/LocationAlarmExternalHandler.kt
new file mode 100644
index 000000000..ed572d1d2
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/external/LocationAlarmExternalHandler.kt
@@ -0,0 +1,77 @@
+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",
+ "iconResName"
+ ))
+
+ 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,
+ alarm.iconResName
+ ))
+ }
+ 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)
+ val targetAction = action ?: remainingPath
+ when (targetAction) {
+ "start" -> {
+ val alarmId = extras?.getString("id") ?: return null
+ repository.saveActiveAlarmId(alarmId)
+ LocationReachedService.start(context)
+ return Bundle().apply { putBoolean("success", true) }
+ }
+ "stop" -> {
+ repository.saveActiveAlarmId(null)
+ 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
new file mode 100644
index 000000000..c158d9c36
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/external/SettingsExternalHandler.kt
@@ -0,0 +1,60 @@
+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?): 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 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 {
+ 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..0e1a042f5 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
+ read, update and control Essentials features and settings