Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<permission android:name="com.sameerasw.permission.ESSENTIALS_AIRSYNC_BRIDGE" android:protectionLevel="signature" />
<uses-permission android:name="com.sameerasw.permission.ESSENTIALS_AIRSYNC_BRIDGE" />

<permission-group
android:name="com.sameerasw.essentials.permission-group.EXTERNAL_CONTROL"
android:label="@string/perm_external_control_label"
android:description="@string/perm_external_control_desc"
android:icon="@drawable/app_logo" />

<permission android:name="com.sameerasw.essentials.permission.EXTERNAL_CONTROL"
android:permissionGroup="com.sameerasw.essentials.permission-group.EXTERNAL_CONTROL"
android:protectionLevel="dangerous"
android:label="@string/perm_external_control_label"
android:description="@string/perm_external_control_desc"
android:icon="@drawable/app_logo" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />


Expand Down Expand Up @@ -909,6 +922,21 @@
android:value="android.service.quicksettings.CATEGORY_DISPLAY" />
</service>

<provider
android:name=".external.ExternalControlProvider"
android:authorities="${applicationId}.external"
android:exported="true"
android:permission="com.sameerasw.essentials.permission.EXTERNAL_CONTROL" />

<receiver
android:name=".external.ExternalActionReceiver"
android:exported="true"
android:permission="com.sameerasw.essentials.permission.EXTERNAL_CONTROL">
<intent-filter>
<action android:name="com.sameerasw.essentials.action.EXTERNAL_CONTROL" />
</intent-filter>
</receiver>

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
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<out String>?): Int {
return 0
}

override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): 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
}
}
Original file line number Diff line number Diff line change
@@ -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?
}
Original file line number Diff line number Diff line change
@@ -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<String, ExternalHandler>()

init {
registerHandler(SettingsExternalHandler())
registerHandler(LocationAlarmExternalHandler())
}

fun registerHandler(handler: ExternalHandler) {
handlers[handler.path] = handler
}

private fun getHandlerAndRemainingPath(fullPath: String): Pair<ExternalHandler, String>? {
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)
}
}
Original file line number Diff line number Diff line change
@@ -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<Any?>(
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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1556,4 +1556,7 @@
<string name="favorites_widget_name">Favorite Features</string>
<string name="favorites_widget_description">View and launch your favorite features at a glance.</string>
<string name="favorites_widget_empty_state">No favorite features pinned yet. Open the app to pin some!</string>
<!-- External Control Permission -->
<string name="perm_external_control_label">control Essentials features</string>
<string name="perm_external_control_desc">read, update and control Essentials features and settings</string>
</resources>