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()) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a4d5c189f..53e9eb8be 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -296,6 +296,15 @@ android:exported="false" android:foregroundServiceType="specialUse" /> + + + + + + + + + diff --git a/app/src/main/java/com/sameerasw/essentials/EssentialsApp.kt b/app/src/main/java/com/sameerasw/essentials/EssentialsApp.kt index 29dc53abc..ad5982dd7 100644 --- a/app/src/main/java/com/sameerasw/essentials/EssentialsApp.kt +++ b/app/src/main/java/com/sameerasw/essentials/EssentialsApp.kt @@ -41,6 +41,7 @@ class EssentialsApp : Application() { com.sameerasw.essentials.domain.diy.DIYRepository.init(this) com.sameerasw.essentials.services.automation.AutomationManager.init(this) com.sameerasw.essentials.services.CalendarSyncManager.init(this) + com.sameerasw.essentials.services.DeviceInfoSyncManager.init(this) com.sameerasw.essentials.utils.ServiceUtils.startRequiredServices(this) initSentry() diff --git a/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt index a498aaabd..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) @@ -610,6 +614,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/DeviceInfoSyncManager.kt b/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt new file mode 100644 index 000000000..c08591e8c --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt @@ -0,0 +1,156 @@ +package com.sameerasw.essentials.services + +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +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 + +object DeviceInfoSyncManager { + private const val TAG = "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() { + syncDeviceInfo(currentContext ?: return) + handler.postDelayed(this, 5 * 60 * 1000) // Sync every 5 minutes + } + } + + private var currentContext: Context? = null + + fun init(context: Context) { + if (isInitialized) return + currentContext = context.applicationContext + isInitialized = true + + // Initial sync + syncDeviceInfo(context) + + // 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)) + + // 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) + + // 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) + } + } + + 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) + } + + 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 plugged: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1 + val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || + status == BatteryManager.BATTERY_STATUS_FULL || + plugged == BatteryManager.BATTERY_PLUGGED_AC || + plugged == BatteryManager.BATTERY_PLUGGED_USB || + 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 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) + 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.putInt("ringer_mode", ringerMode) + dataMap.putString("device_name", deviceName) + dataMap.putLong("timestamp", System.currentTimeMillis()) + + val putDataReq = putDataMapReq.asPutDataRequest() + putDataReq.setUrgent() + + Wearable.getDataClient(context).putDataItem(putDataReq) + } +} 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..65d13dace --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/EssentialsWearableListenerService.kt @@ -0,0 +1,62 @@ +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) { + 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) + } + "/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/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/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/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/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 + } + } 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..1878d448e 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/WatchViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/WatchViewModel.kt @@ -5,15 +5,30 @@ 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 connectedWatchName = mutableStateOf(null) + 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) 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/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..ac2619e61 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -447,7 +447,15 @@ 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 + Connected + Unknown Watch Overlay Frame Device Brand