From a943fce521a15bd87b39e5f0fae5f750e9740a73 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Tue, 5 May 2026 14:32:26 +0530 Subject: [PATCH 1/8] feat: implement DeviceInfoSyncManager and WearableListenerService for periodic device status synchronization --- app/src/main/AndroidManifest.xml | 9 +++ .../com/sameerasw/essentials/EssentialsApp.kt | 1 + .../services/DeviceInfoSyncManager.kt | 77 +++++++++++++++++++ .../EssentialsWearableListenerService.kt | 19 +++++ 4 files changed, 106 insertions(+) create mode 100644 app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/services/EssentialsWearableListenerService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a4d5c189f..e842f6acf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -296,6 +296,15 @@ android:exported="false" android:foregroundServiceType="specialUse" /> + + + + + + + + context.registerReceiver(null, ifilter) + } + + val level: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1 + val scale: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1 + val batteryPct = if (level != -1 && scale != -1) (level / scale.toFloat() * 100).toInt() else -1 + + val status: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1 + val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || + status == BatteryManager.BATTERY_STATUS_FULL + + Log.d(TAG, "Syncing device info: Battery=$batteryPct%, Charging=$isCharging") + + val putDataMapReq = PutDataMapRequest.create(SYNC_PATH) + val dataMap = putDataMapReq.dataMap + dataMap.putInt("battery_level", batteryPct) + dataMap.putBoolean("is_charging", isCharging) + dataMap.putLong("timestamp", System.currentTimeMillis()) + + val putDataReq = putDataMapReq.asPutDataRequest() + putDataReq.setUrgent() + + Wearable.getDataClient(context).putDataItem(putDataReq) + .addOnSuccessListener { + Log.d(TAG, "Successfully synced device info to wearable") + } + .addOnFailureListener { e -> + Log.e(TAG, "Failed to sync device info to wearable", e) + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/services/EssentialsWearableListenerService.kt b/app/src/main/java/com/sameerasw/essentials/services/EssentialsWearableListenerService.kt new file mode 100644 index 000000000..c123b1dbf --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/EssentialsWearableListenerService.kt @@ -0,0 +1,19 @@ +package com.sameerasw.essentials.services + +import android.util.Log +import com.google.android.gms.wearable.MessageEvent +import com.google.android.gms.wearable.WearableListenerService + +class EssentialsWearableListenerService : WearableListenerService() { + companion object { + private const val TAG = "EssentialsWearableListener" + private const val PATH_REQUEST_SYNC = "/request_device_info_sync" + } + + override fun onMessageReceived(messageEvent: MessageEvent) { + Log.d(TAG, "onMessageReceived: ${messageEvent.path}") + if (messageEvent.path == PATH_REQUEST_SYNC) { + DeviceInfoSyncManager.forceSync(this) + } + } +} From bac1fd4c3dda4a48cca84f5d79a08c4b0c3059ac Mon Sep 17 00:00:00 2001 From: sameerasw Date: Tue, 5 May 2026 14:58:13 +0530 Subject: [PATCH 2/8] feat: register battery change receiver and refine charging status detection in DeviceInfoSyncManager --- .../essentials/services/DeviceInfoSyncManager.kt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt b/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt index d1dffe06a..2b6e79d83 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt @@ -35,6 +35,13 @@ object DeviceInfoSyncManager { // Start periodic sync handler.postDelayed(syncRunnable, 5 * 60 * 1000) + + // Sync on battery change + context.registerReceiver(object : android.content.BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + syncDeviceInfo(context) + } + }, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) } fun forceSync(context: Context) { @@ -52,10 +59,14 @@ object DeviceInfoSyncManager { val batteryPct = if (level != -1 && scale != -1) (level / scale.toFloat() * 100).toInt() else -1 val status: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1 + val plugged: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1 val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || - status == BatteryManager.BATTERY_STATUS_FULL + status == BatteryManager.BATTERY_STATUS_FULL || + plugged == BatteryManager.BATTERY_PLUGGED_AC || + plugged == BatteryManager.BATTERY_PLUGGED_USB || + plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS - Log.d(TAG, "Syncing device info: Battery=$batteryPct%, Charging=$isCharging") + Log.d(TAG, "syncDeviceInfo: Battery=$batteryPct%, Status=$status, Plugged=$plugged, isCharging=$isCharging") val putDataMapReq = PutDataMapRequest.create(SYNC_PATH) val dataMap = putDataMapReq.dataMap From 61b70f45124388abcf3dbbf6bec8b46021127a4c Mon Sep 17 00:00:00 2001 From: sameerasw Date: Tue, 5 May 2026 15:52:38 +0530 Subject: [PATCH 3/8] feat: sync flashlight state and intensity with wearable and add remote control actions --- app/src/main/AndroidManifest.xml | 4 +- .../services/DeviceInfoSyncManager.kt | 77 ++++++++++++++++--- .../EssentialsWearableListenerService.kt | 27 ++++++- .../tiles/ScreenOffAccessibilityService.kt | 13 ++++ .../essentials/utils/FlashlightUtil.kt | 29 +++++++ 5 files changed, 135 insertions(+), 15 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e842f6acf..53e9eb8be 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -301,7 +301,7 @@ android:exported="true"> - + @@ -689,6 +689,8 @@ + + diff --git a/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt b/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt index 2b6e79d83..d99c48574 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt @@ -7,6 +7,8 @@ import android.os.BatteryManager import android.os.Handler import android.os.Looper import android.util.Log +import android.hardware.camera2.CameraManager +import com.sameerasw.essentials.utils.FlashlightUtil import com.google.android.gms.wearable.PutDataMapRequest import com.google.android.gms.wearable.Wearable @@ -15,6 +17,42 @@ object DeviceInfoSyncManager { private const val SYNC_PATH = "/device_info" private val handler = Handler(Looper.getMainLooper()) private var isInitialized = false + + private var isTorchOn = false + private var torchLevel = 1 + private var maxTorchLevel = 1 + private var isIntensitySupported = false + + private val torchCallback = object : CameraManager.TorchCallback() { + override fun onTorchModeChanged(cameraId: String, enabled: Boolean) { + val context = currentContext ?: return + val primaryId = FlashlightUtil.getCameraId(context) + if (cameraId == primaryId) { + isTorchOn = enabled + + var level = FlashlightUtil.getCurrentLevel(context, cameraId) + // Fallback to last known intensity if system returns default level 1 + if (enabled && level <= 1) { + val prefs = context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) + level = prefs.getInt("flashlight_last_intensity", level) + } + + torchLevel = level + maxTorchLevel = FlashlightUtil.getMaxLevel(context, cameraId) + isIntensitySupported = FlashlightUtil.isIntensitySupported(context, cameraId) + syncDeviceInfo(context) + } + } + + override fun onTorchStrengthLevelChanged(cameraId: String, newStrengthLevel: Int) { + val context = currentContext ?: return + val primaryId = FlashlightUtil.getCameraId(context) + if (cameraId == primaryId) { + torchLevel = newStrengthLevel + syncDeviceInfo(context) + } + } + } private val syncRunnable = object : Runnable { override fun run() { @@ -42,14 +80,35 @@ object DeviceInfoSyncManager { syncDeviceInfo(context) } }, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) + + // Sync on flashlight change + val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager + cameraManager.registerTorchCallback(torchCallback, handler) + + // Get initial flashlight state + val id = FlashlightUtil.getCameraId(context) + if (id != null) { + isIntensitySupported = FlashlightUtil.isIntensitySupported(context, id) + maxTorchLevel = FlashlightUtil.getMaxLevel(context, id) + torchLevel = FlashlightUtil.getCurrentLevel(context, id) + } } - fun forceSync(context: Context) { - Log.d(TAG, "forceSync: Manually triggering sync") - syncDeviceInfo(context) + private val syncDebouncer = Runnable { + currentContext?.let { performSync(it) } } private fun syncDeviceInfo(context: Context) { + handler.removeCallbacks(syncDebouncer) + handler.postDelayed(syncDebouncer, 250L) + } + + fun forceSync(context: Context) { + handler.removeCallbacks(syncDebouncer) + performSync(context) + } + + private fun performSync(context: Context) { val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { ifilter -> context.registerReceiver(null, ifilter) } @@ -66,23 +125,19 @@ object DeviceInfoSyncManager { plugged == BatteryManager.BATTERY_PLUGGED_USB || plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS - Log.d(TAG, "syncDeviceInfo: Battery=$batteryPct%, Status=$status, Plugged=$plugged, isCharging=$isCharging") - val putDataMapReq = PutDataMapRequest.create(SYNC_PATH) val dataMap = putDataMapReq.dataMap dataMap.putInt("battery_level", batteryPct) dataMap.putBoolean("is_charging", isCharging) + dataMap.putBoolean("flashlight_on", isTorchOn) + dataMap.putInt("flashlight_level", torchLevel) + dataMap.putInt("flashlight_max_level", maxTorchLevel) + dataMap.putBoolean("flashlight_intensity_supported", isIntensitySupported) dataMap.putLong("timestamp", System.currentTimeMillis()) val putDataReq = putDataMapReq.asPutDataRequest() putDataReq.setUrgent() Wearable.getDataClient(context).putDataItem(putDataReq) - .addOnSuccessListener { - Log.d(TAG, "Successfully synced device info to wearable") - } - .addOnFailureListener { e -> - Log.e(TAG, "Failed to sync device info to wearable", e) - } } } diff --git a/app/src/main/java/com/sameerasw/essentials/services/EssentialsWearableListenerService.kt b/app/src/main/java/com/sameerasw/essentials/services/EssentialsWearableListenerService.kt index c123b1dbf..96d27277a 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/EssentialsWearableListenerService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/EssentialsWearableListenerService.kt @@ -11,9 +11,30 @@ class EssentialsWearableListenerService : WearableListenerService() { } override fun onMessageReceived(messageEvent: MessageEvent) { - Log.d(TAG, "onMessageReceived: ${messageEvent.path}") - if (messageEvent.path == PATH_REQUEST_SYNC) { - DeviceInfoSyncManager.forceSync(this) + super.onMessageReceived(messageEvent) + + when (messageEvent.path) { + PATH_REQUEST_SYNC -> { + DeviceInfoSyncManager.forceSync(this) + } + "/toggle_flashlight" -> { + val intent = android.content.Intent(this, com.sameerasw.essentials.services.receivers.FlashlightActionReceiver::class.java).apply { + action = com.sameerasw.essentials.services.receivers.FlashlightActionReceiver.ACTION_TOGGLE + } + sendBroadcast(intent) + } + "/set_flashlight_intensity" -> { + val intensity = try { + String(messageEvent.data).toInt() + } catch (e: Exception) { + 1 + } + val intent = android.content.Intent(this, com.sameerasw.essentials.services.receivers.FlashlightActionReceiver::class.java).apply { + action = com.sameerasw.essentials.services.receivers.FlashlightActionReceiver.ACTION_SET_INTENSITY + putExtra(com.sameerasw.essentials.services.receivers.FlashlightActionReceiver.EXTRA_INTENSITY, intensity) + } + sendBroadcast(intent) + } } } } diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt index 13c57487d..3e699069b 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt @@ -110,6 +110,14 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene "FORCE_TURN_OFF_AOD" -> { aodForceTurnOffHandler.forceTurnOff() } + + FlashlightActionReceiver.ACTION_TOGGLE, + FlashlightActionReceiver.ACTION_OFF, + FlashlightActionReceiver.ACTION_SET_INTENSITY, + FlashlightActionReceiver.ACTION_INCREASE, + FlashlightActionReceiver.ACTION_DECREASE -> { + flashlightHandler.handleIntent(intent) + } } } } @@ -120,6 +128,11 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene addAction(InputEventListenerService.ACTION_VOLUME_LONG_PRESSED) addAction("SHOW_AMBIENT_GLANCE") addAction("FORCE_TURN_OFF_AOD") + addAction(FlashlightActionReceiver.ACTION_TOGGLE) + addAction(FlashlightActionReceiver.ACTION_OFF) + addAction(FlashlightActionReceiver.ACTION_SET_INTENSITY) + addAction(FlashlightActionReceiver.ACTION_INCREASE) + addAction(FlashlightActionReceiver.ACTION_DECREASE) } registerReceiver(screenReceiver, filter, RECEIVER_EXPORTED) diff --git a/app/src/main/java/com/sameerasw/essentials/utils/FlashlightUtil.kt b/app/src/main/java/com/sameerasw/essentials/utils/FlashlightUtil.kt index 747676c61..30c63df5e 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/FlashlightUtil.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/FlashlightUtil.kt @@ -170,4 +170,33 @@ object FlashlightUtil { return fadeFlashlight(context, cameraId, currentLevel, targetLevel, durationMs, steps) } + fun getCameraId(context: Context): String? { + val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager + try { + var targetCameraId: String? = null + for (id in cameraManager.cameraIdList) { + val chars = cameraManager.getCameraCharacteristics(id) + val flashAvailable = chars.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false + val lensFacing = chars.get(CameraCharacteristics.LENS_FACING) + if (flashAvailable && lensFacing == CameraCharacteristics.LENS_FACING_BACK) { + targetCameraId = id + break + } + } + if (targetCameraId == null) { + for (id in cameraManager.cameraIdList) { + val chars = cameraManager.getCameraCharacteristics(id) + if (chars.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) == true) { + targetCameraId = id + break + } + } + } + return targetCameraId + } catch (e: Exception) { + Log.e(TAG, "Error getting camera ID", e) + } + return null + } + } From ca3e5febc6591289db32b326c0f7815c48a21af3 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Tue, 5 May 2026 15:59:25 +0530 Subject: [PATCH 4/8] feat: add ringer mode synchronization and support for toggling sound mode via wearable commands --- .../essentials/services/DeviceInfoSyncManager.kt | 10 ++++++++++ .../services/EssentialsWearableListenerService.kt | 3 +++ 2 files changed, 13 insertions(+) diff --git a/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt b/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt index d99c48574..74cb6b8a3 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt @@ -81,6 +81,13 @@ object DeviceInfoSyncManager { } }, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) + // Sync on ringer mode change + context.registerReceiver(object : android.content.BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + syncDeviceInfo(context) + } + }, IntentFilter(android.media.AudioManager.RINGER_MODE_CHANGED_ACTION)) + // Sync on flashlight change val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager cameraManager.registerTorchCallback(torchCallback, handler) @@ -126,6 +133,8 @@ object DeviceInfoSyncManager { plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS val putDataMapReq = PutDataMapRequest.create(SYNC_PATH) + val ringerMode = (context.getSystemService(Context.AUDIO_SERVICE) as? android.media.AudioManager)?.ringerMode ?: 2 + val dataMap = putDataMapReq.dataMap dataMap.putInt("battery_level", batteryPct) dataMap.putBoolean("is_charging", isCharging) @@ -133,6 +142,7 @@ object DeviceInfoSyncManager { dataMap.putInt("flashlight_level", torchLevel) dataMap.putInt("flashlight_max_level", maxTorchLevel) dataMap.putBoolean("flashlight_intensity_supported", isIntensitySupported) + dataMap.putInt("ringer_mode", ringerMode) dataMap.putLong("timestamp", System.currentTimeMillis()) val putDataReq = putDataMapReq.asPutDataRequest() diff --git a/app/src/main/java/com/sameerasw/essentials/services/EssentialsWearableListenerService.kt b/app/src/main/java/com/sameerasw/essentials/services/EssentialsWearableListenerService.kt index 96d27277a..7349e0fa1 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/EssentialsWearableListenerService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/EssentialsWearableListenerService.kt @@ -35,6 +35,9 @@ class EssentialsWearableListenerService : WearableListenerService() { } sendBroadcast(intent) } + "/toggle_sound_mode" -> { + com.sameerasw.essentials.services.handlers.SoundModeHandler(this).cycleNextMode() + } } } } From 6d7472873414adc791f74669b9493dc2523166e3 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Tue, 5 May 2026 16:08:20 +0530 Subject: [PATCH 5/8] feat: include device name in synced data map in DeviceInfoSyncManager --- .../com/sameerasw/essentials/services/DeviceInfoSyncManager.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt b/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt index 74cb6b8a3..c08591e8c 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt @@ -134,6 +134,8 @@ object DeviceInfoSyncManager { val putDataMapReq = PutDataMapRequest.create(SYNC_PATH) val ringerMode = (context.getSystemService(Context.AUDIO_SERVICE) as? android.media.AudioManager)?.ringerMode ?: 2 + val deviceName = android.provider.Settings.Global.getString(context.contentResolver, android.provider.Settings.Global.DEVICE_NAME) + ?: android.os.Build.MODEL val dataMap = putDataMapReq.dataMap dataMap.putInt("battery_level", batteryPct) @@ -143,6 +145,7 @@ object DeviceInfoSyncManager { dataMap.putInt("flashlight_max_level", maxTorchLevel) dataMap.putBoolean("flashlight_intensity_supported", isIntensitySupported) dataMap.putInt("ringer_mode", ringerMode) + dataMap.putString("device_name", deviceName) dataMap.putLong("timestamp", System.currentTimeMillis()) val putDataReq = putDataMapReq.asPutDataRequest() From 6e38f1c602667603c475118fa075b1bf2afd95a6 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Tue, 5 May 2026 19:07:57 +0530 Subject: [PATCH 6/8] feat: implement remote device locking from watch with configurable lock modes --- .../essentials/FeatureSettingsActivity.kt | 9 ++ .../data/repository/SettingsRepository.kt | 1 + .../domain/registry/FeatureRegistry.kt | 15 +++ .../EssentialsWearableListenerService.kt | 19 +++ .../configs/RemoteLockSettingsUI.kt | 113 ++++++++++++++++++ .../essentials/utils/PermissionUtils.kt | 11 ++ .../essentials/viewmodels/WatchViewModel.kt | 12 ++ app/src/main/res/drawable/rounded_lock_24.xml | 20 ++++ app/src/main/res/values/strings.xml | 8 +- 9 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/sameerasw/essentials/ui/composables/configs/RemoteLockSettingsUI.kt create mode 100644 app/src/main/res/drawable/rounded_lock_24.xml diff --git a/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt index a498aaabd..99ef0b8e4 100644 --- a/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt @@ -610,6 +610,15 @@ class FeatureSettingsActivity : AppCompatActivity() { ) } + "Lock from Watch" -> { + com.sameerasw.essentials.ui.composables.configs.RemoteLockSettingsUI( + mainViewModel = viewModel, + watchViewModel = watchViewModel, + modifier = Modifier.padding(top = 16.dp), + highlightSetting = highlightSetting + ) + } + "Maps power saving mode" -> { MapsPowerSavingSettingsUI( viewModel = viewModel, diff --git a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt index 27bc2a7a6..75bec9ff7 100644 --- a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt +++ b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt @@ -176,6 +176,7 @@ class SettingsRepository(private val context: Context) { const val KEY_CALENDAR_SYNC_ENABLED = "calendar_sync_enabled" const val KEY_CALENDAR_SYNC_SELECTED_CALENDARS = "calendar_sync_selected_calendars" const val KEY_CALENDAR_SYNC_PERIODIC_ENABLED = "calendar_sync_periodic_enabled" + const val KEY_REMOTE_LOCK_MODE = "remote_lock_mode" // 0: Screen off, 1: Lock const val KEY_GITHUB_ACCESS_TOKEN = "github_access_token" const val KEY_GITHUB_USER_PROFILE = "github_user_profile" diff --git a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt index e36dc303d..a8f66c95b 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt @@ -964,6 +964,21 @@ object FeatureRegistry { override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) = viewModel.setCalendarSyncEnabled(enabled, context) }, + + object : Feature( + id = "Lock from Watch", + title = R.string.feat_lock_from_watch_title, + iconRes = R.drawable.rounded_lock_24, + category = R.string.cat_tools, + description = R.string.feat_lock_from_watch_desc, + aboutDescription = R.string.feat_lock_from_watch_desc, + parentFeatureId = "Watch", + hasMoreSettings = true, + showToggle = false + ) { + override fun isEnabled(viewModel: MainViewModel) = true + override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {} + }, object : Feature( diff --git a/app/src/main/java/com/sameerasw/essentials/services/EssentialsWearableListenerService.kt b/app/src/main/java/com/sameerasw/essentials/services/EssentialsWearableListenerService.kt index 7349e0fa1..65d13dace 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/EssentialsWearableListenerService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/EssentialsWearableListenerService.kt @@ -38,6 +38,25 @@ class EssentialsWearableListenerService : WearableListenerService() { "/toggle_sound_mode" -> { com.sameerasw.essentials.services.handlers.SoundModeHandler(this).cycleNextMode() } + "/lock_device" -> { + val repository = com.sameerasw.essentials.data.repository.SettingsRepository(this) + val mode = repository.getInt(com.sameerasw.essentials.data.repository.SettingsRepository.KEY_REMOTE_LOCK_MODE, 0) + + if (mode == 1) { + // Device Admin Lock + val dpm = getSystemService(android.content.Context.DEVICE_POLICY_SERVICE) as android.app.admin.DevicePolicyManager + val adminComponent = android.content.ComponentName(this, com.sameerasw.essentials.services.receivers.SecurityDeviceAdminReceiver::class.java) + if (dpm.isAdminActive(adminComponent)) { + dpm.lockNow() + } + } else { + // Accessibility Lock + val intent = android.content.Intent(this, com.sameerasw.essentials.services.tiles.ScreenOffAccessibilityService::class.java).apply { + action = "LOCK_SCREEN" + } + startService(intent) + } + } } } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/RemoteLockSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/RemoteLockSettingsUI.kt new file mode 100644 index 000000000..88fc0866c --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/RemoteLockSettingsUI.kt @@ -0,0 +1,113 @@ +package com.sameerasw.essentials.ui.composables.configs + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.sameerasw.essentials.R +import com.sameerasw.essentials.data.repository.SettingsRepository +import com.sameerasw.essentials.ui.components.cards.ConfigPickerItem +import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer +import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem +import com.sameerasw.essentials.ui.modifiers.highlight +import com.sameerasw.essentials.utils.PermissionUtils +import com.sameerasw.essentials.viewmodels.MainViewModel +import com.sameerasw.essentials.viewmodels.WatchViewModel + +@Composable +fun RemoteLockSettingsUI( + mainViewModel: MainViewModel, + watchViewModel: WatchViewModel, + modifier: Modifier = Modifier, + highlightSetting: String? = null +) { + val context = LocalContext.current + val settingsRepository = remember { SettingsRepository(context) } + var showPermissionSheet by remember { mutableStateOf(false) } + + val isAccessibilityEnabled by mainViewModel.isAccessibilityEnabled + val isDeviceAdminEnabled by mainViewModel.isDeviceAdminEnabled + val remoteLockMode by watchViewModel.remoteLockMode + + LaunchedEffect(Unit) { + watchViewModel.load(settingsRepository) + } + + if (showPermissionSheet) { + com.sameerasw.essentials.ui.components.sheets.PermissionsBottomSheet( + onDismissRequest = { showPermissionSheet = false }, + featureTitle = R.string.feat_lock_from_watch_title, + permissions = listOf( + com.sameerasw.essentials.ui.components.sheets.PermissionItem( + iconRes = R.drawable.rounded_settings_accessibility_24, + title = R.string.perm_accessibility_title, + description = R.string.perm_accessibility_desc_common, + dependentFeatures = listOf(R.string.feat_lock_from_watch_title), + actionLabel = R.string.perm_action_enable, + action = { PermissionUtils.openAccessibilitySettings(context) }, + isGranted = isAccessibilityEnabled + ), + com.sameerasw.essentials.ui.components.sheets.PermissionItem( + iconRes = R.drawable.rounded_security_24, + title = R.string.perm_device_admin_title, + description = R.string.perm_device_admin_desc, + dependentFeatures = listOf(R.string.feat_lock_from_watch_title), + actionLabel = R.string.perm_action_grant, + action = { mainViewModel.requestDeviceAdmin(context) }, + isGranted = isDeviceAdminEnabled + ) + ) + ) + } + + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp) + ) { + Text( + text = stringResource(R.string.remote_lock_mode_title), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 8.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + RoundedCardContainer { + com.sameerasw.essentials.ui.components.pickers.SegmentedPicker( + items = listOf(0, 1), + selectedItem = remoteLockMode, + onItemSelected = { mode -> + val hasPermission = when (mode) { + 0 -> isAccessibilityEnabled + 1 -> isDeviceAdminEnabled + else -> false + } + if (!hasPermission) { + showPermissionSheet = true + } else { + watchViewModel.setRemoteLockMode(mode, settingsRepository) + } + }, + labelProvider = { mode -> + when (mode) { + 0 -> context.getString(R.string.remote_lock_mode_screen_off) + 1 -> context.getString(R.string.remote_lock_mode_lock) + else -> "" + } + }, + modifier = Modifier.highlight(highlightSetting == "remote_lock_mode") + ) + } + + Text( + text = stringResource(R.string.remote_lock_mode_admin_note), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 12.dp, start = 16.dp, end = 16.dp) + ) + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/utils/PermissionUtils.kt b/app/src/main/java/com/sameerasw/essentials/utils/PermissionUtils.kt index 3ede76411..235ed8434 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/PermissionUtils.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/PermissionUtils.kt @@ -258,4 +258,15 @@ object PermissionUtils { } catch (_: Exception) { } } + + fun openDeviceAdminSettings(context: Context) { + try { + val intent = Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN) + val adminComponent = ComponentName(context, SecurityDeviceAdminReceiver::class.java) + intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, adminComponent) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } catch (e: Exception) { + } + } } diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/WatchViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/WatchViewModel.kt index 9b80b0d2d..ebf0f9a5e 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/WatchViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/WatchViewModel.kt @@ -5,8 +5,20 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.google.android.gms.wearable.Wearable +import com.sameerasw.essentials.data.repository.SettingsRepository + class WatchViewModel : ViewModel() { val isWatchDetected = mutableStateOf(false) + val remoteLockMode = mutableStateOf(0) // 0: Screen off, 1: Lock + + fun load(repository: SettingsRepository) { + remoteLockMode.value = repository.getInt(SettingsRepository.KEY_REMOTE_LOCK_MODE, 0) + } + + fun setRemoteLockMode(mode: Int, repository: SettingsRepository) { + remoteLockMode.value = mode + repository.putInt(SettingsRepository.KEY_REMOTE_LOCK_MODE, mode) + } fun check(context: Context) { val nodeClient = Wearable.getNodeClient(context) diff --git a/app/src/main/res/drawable/rounded_lock_24.xml b/app/src/main/res/drawable/rounded_lock_24.xml new file mode 100644 index 000000000..427742d4f --- /dev/null +++ b/app/src/main/res/drawable/rounded_lock_24.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 173b6874d..4eb044b10 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -447,7 +447,13 @@ Always on Display Show time and info while screen off Calendar Sync - Sync events to your watch + Sync your phone calendar to your watch + Lock from Watch + Lock your phone remotely + Lock Mode + Screen off + Lock + Using device admin lock will disable biometrics Overlay Frame Device Brand From f0a4f8b0032da76a674b8d73acd94b3c80b6112a Mon Sep 17 00:00:00 2001 From: sameerasw Date: Tue, 5 May 2026 19:18:08 +0530 Subject: [PATCH 7/8] feat: add watch connection status UI and display connected watch name in WatchSettings --- .../essentials/FeatureSettingsActivity.kt | 4 ++ .../ui/composables/configs/WatchSettingsUI.kt | 64 ++++++++++++++++--- .../essentials/viewmodels/WatchViewModel.kt | 3 + app/src/main/res/values/strings.xml | 2 + 4 files changed, 64 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt index 99ef0b8e4..6cab6273f 100644 --- a/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt @@ -359,6 +359,10 @@ class FeatureSettingsActivity : AppCompatActivity() { } if (featureId == "Watch") { + val context = androidx.compose.ui.platform.LocalContext.current + LaunchedEffect(Unit) { + watchViewModel.check(context) + } WatchSettingsUI( viewModel = watchViewModel, modifier = Modifier.padding(top = 16.dp) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/WatchSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/WatchSettingsUI.kt index 8a04174e5..3f8204972 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/WatchSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/WatchSettingsUI.kt @@ -1,18 +1,16 @@ package com.sameerasw.essentials.ui.composables.configs import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.sameerasw.essentials.R @@ -24,16 +22,64 @@ fun WatchSettingsUI( viewModel: WatchViewModel, modifier: Modifier = Modifier ) { - LocalContext.current val uriHandler = LocalUriHandler.current val isWatchDetected = viewModel.isWatchDetected.value + val connectedWatchName = viewModel.connectedWatchName.value Column( modifier = modifier .fillMaxWidth() .padding(horizontal = 16.dp) ) { - if (!isWatchDetected) { + if (isWatchDetected) { + RoundedCardContainer( + modifier = Modifier + .padding(bottom = 18.dp) + .fillMaxWidth(), + cornerRadius = 24.dp + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .height(280.dp) + .background(MaterialTheme.colorScheme.surfaceBright) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier + .size(100.dp) + .background( + color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_watch_24), + contentDescription = null, + modifier = Modifier.size(56.dp), + tint = MaterialTheme.colorScheme.primary + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + + Text( + text = connectedWatchName ?: stringResource(R.string.watch_unknown_name), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface + ) + + Text( + text = stringResource(R.string.watch_connected_status), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary + ) + } + } + } else { RoundedCardContainer( modifier = Modifier .padding(bottom = 18.dp) diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/WatchViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/WatchViewModel.kt index ebf0f9a5e..1878d448e 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/WatchViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/WatchViewModel.kt @@ -9,6 +9,7 @@ import com.sameerasw.essentials.data.repository.SettingsRepository class WatchViewModel : ViewModel() { val isWatchDetected = mutableStateOf(false) + val connectedWatchName = mutableStateOf(null) val remoteLockMode = mutableStateOf(0) // 0: Screen off, 1: Lock fun load(repository: SettingsRepository) { @@ -24,8 +25,10 @@ class WatchViewModel : ViewModel() { val nodeClient = Wearable.getNodeClient(context) nodeClient.connectedNodes.addOnSuccessListener { nodes -> isWatchDetected.value = nodes.isNotEmpty() + connectedWatchName.value = nodes.firstOrNull()?.displayName }.addOnFailureListener { isWatchDetected.value = false + connectedWatchName.value = null } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4eb044b10..ac2619e61 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -454,6 +454,8 @@ Screen off Lock Using device admin lock will disable biometrics + Connected + Unknown Watch Overlay Frame Device Brand From 3bb12c47efa832d69b52ff59b2b5f1b8ed65fc5c Mon Sep 17 00:00:00 2001 From: sameerasw Date: Tue, 5 May 2026 20:28:44 +0530 Subject: [PATCH 8/8] chore: bump version code to 42 and version name to 13.3 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d6a0581e8..16ad43dd5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,8 +21,8 @@ android { applicationId = "com.sameerasw.essentials" minSdk = 26 targetSdk = 36 - versionCode = 41 - versionName = "13.2" + versionCode = 42 + versionName = "13.3" val whatsNewCounter = 1 buildConfigField("int", "WHATS_NEW_COUNTER", whatsNewCounter.toString())