From 8ee96ec68f1b0462b15fb2f4eabc5f5e02555313 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 30 Apr 2026 20:58:50 +0530 Subject: [PATCH 1/6] refactor: #369 remove hide gesture bar dependency from circle to search overlay logic --- .../services/tiles/ScreenOffAccessibilityService.kt | 5 ++--- .../ui/composables/configs/OtherCustomizationsSettingsUI.kt | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) 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 c8f620ea6..cb744c1ce 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 @@ -55,7 +55,7 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene private val preferenceChangeListener = android.content.SharedPreferences.OnSharedPreferenceChangeListener { _, key -> - if (key == "circle_to_search_gesture_enabled" || key == "hide_gesture_bar_enabled") { + if (key == "circle_to_search_gesture_enabled") { updateOmniOverlay() } } @@ -165,9 +165,8 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene private fun updateOmniOverlay() { val prefs = getSharedPreferences("essentials_prefs", MODE_PRIVATE) - val isHideBarEnabled = prefs.getBoolean("hide_gesture_bar_enabled", false) val isGestureEnabled = prefs.getBoolean("circle_to_search_gesture_enabled", false) - omniGestureOverlayHandler.updateOverlay(isHideBarEnabled && isGestureEnabled) + omniGestureOverlayHandler.updateOverlay(isGestureEnabled) } override fun onDestroy() { diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/OtherCustomizationsSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/OtherCustomizationsSettingsUI.kt index a8f2f3835..5bd1f52f7 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/OtherCustomizationsSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/OtherCustomizationsSettingsUI.kt @@ -185,7 +185,7 @@ fun OtherCustomizationsSettingsUI( requestingPermissionFor = PermissionModule.CIRCLE_TO_SEARCH } }, - enabled = viewModel.isHideGestureBarEnabled.value || viewModel.isHideGestureBarOnLauncherEnabled.value, + enabled = true, onDisabledClick = { if (!isShellGranted || !isAccessibilityEnabled) { requestingPermissionFor = PermissionModule.CIRCLE_TO_SEARCH From 1a03d3fbf5a070b987300fb018dd7cae21eb9409 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 30 Apr 2026 21:34:39 +0530 Subject: [PATCH 2/6] feat: #380 add configurable height and preview visibility settings for Circle to Search gesture --- .../data/repository/SettingsRepository.kt | 2 + .../handlers/OmniGestureOverlayHandler.kt | 34 +++++++----- .../tiles/ScreenOffAccessibilityService.kt | 8 ++- .../configs/OtherCustomizationsSettingsUI.kt | 55 ++++++++++++++++--- .../essentials/viewmodels/MainViewModel.kt | 22 ++++++++ app/src/main/res/values/strings.xml | 2 + 6 files changed, 99 insertions(+), 24 deletions(-) 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 d9ca903c5..c1994dfe9 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 @@ -115,6 +115,8 @@ class SettingsRepository(private val context: Context) { const val KEY_HIDE_GESTURE_BAR_ENABLED = "hide_gesture_bar_enabled" const val KEY_HIDE_GESTURE_BAR_ON_LAUNCHER_ENABLED = "hide_gesture_bar_on_launcher_enabled" const val KEY_CIRCLE_TO_SEARCH_GESTURE_ENABLED = "circle_to_search_gesture_enabled" + const val KEY_CIRCLE_TO_SEARCH_GESTURE_HEIGHT = "circle_to_search_gesture_height" + const val KEY_CIRCLE_TO_SEARCH_PREVIEW_ENABLED = "circle_to_search_preview_enabled" const val KEY_AUTO_UPDATE_ENABLED = "auto_update_enabled" const val KEY_UPDATE_NOTIFICATION_ENABLED = "update_notification_enabled" const val KEY_LAST_UPDATE_CHECK_TIME = "last_update_check_time" diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/OmniGestureOverlayHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/OmniGestureOverlayHandler.kt index 888398ac9..9be2145db 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/OmniGestureOverlayHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/OmniGestureOverlayHandler.kt @@ -37,26 +37,16 @@ class OmniGestureOverlayHandler(private val service: AccessibilityService) { runCatching { VibrationEffect.createWaveform(timings, amplitudes, -1) }.getOrNull() } - fun updateOverlay(enabled: Boolean) { + fun updateOverlay(enabled: Boolean, heightDp: Float = 48f, isPreview: Boolean = false) { handler.post { - if (enabled) showOverlay() else removeOverlay() + if (enabled) showOverlay(heightDp, isPreview) else removeOverlay() } } - private fun showOverlay() { - if (overlayView != null) return - - overlayView = View(service).apply { - setBackgroundColor(Color.TRANSPARENT) - setOnTouchListener { _, event -> - handleTouch(event) - true - } - } - + private fun showOverlay(heightDp: Float, isPreview: Boolean) { val params = WindowManager.LayoutParams( dpToPx(WIDTH_DP), - dpToPx(HEIGHT_DP), + dpToPx(heightDp), WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or @@ -69,7 +59,21 @@ class OmniGestureOverlayHandler(private val service: AccessibilityService) { } } - runCatching { windowManager.addView(overlayView, params) } + if (overlayView == null) { + overlayView = View(service).apply { + setBackgroundColor(if (isPreview) Color.parseColor("#406200EE") else Color.TRANSPARENT) + setOnTouchListener { _, event -> + handleTouch(event) + true + } + } + runCatching { windowManager.addView(overlayView, params) } + } else { + overlayView?.apply { + setBackgroundColor(if (isPreview) Color.parseColor("#406200EE") else Color.TRANSPARENT) + runCatching { windowManager.updateViewLayout(this, params) } + } + } } private fun handleTouch(event: MotionEvent) { 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 cb744c1ce..997684edc 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 @@ -55,7 +55,9 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene private val preferenceChangeListener = android.content.SharedPreferences.OnSharedPreferenceChangeListener { _, key -> - if (key == "circle_to_search_gesture_enabled") { + if (key == "circle_to_search_gesture_enabled" || + key == "circle_to_search_gesture_height" || + key == "circle_to_search_preview_enabled") { updateOmniOverlay() } } @@ -166,7 +168,9 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene private fun updateOmniOverlay() { val prefs = getSharedPreferences("essentials_prefs", MODE_PRIVATE) val isGestureEnabled = prefs.getBoolean("circle_to_search_gesture_enabled", false) - omniGestureOverlayHandler.updateOverlay(isGestureEnabled) + val height = try { prefs.getFloat("circle_to_search_gesture_height", 48f) } catch (e: Exception) { 48f } + val isPreview = prefs.getBoolean("circle_to_search_preview_enabled", false) + omniGestureOverlayHandler.updateOverlay(isGestureEnabled, height, isPreview) } override fun onDestroy() { diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/OtherCustomizationsSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/OtherCustomizationsSettingsUI.kt index 5bd1f52f7..f42b95b3e 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/OtherCustomizationsSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/OtherCustomizationsSettingsUI.kt @@ -23,7 +23,15 @@ import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer import com.sameerasw.essentials.ui.components.sheets.PermissionItem import com.sameerasw.essentials.ui.components.sheets.PermissionsBottomSheet import com.sameerasw.essentials.ui.modifiers.highlight +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.runtime.DisposableEffect +import com.sameerasw.essentials.ui.components.sliders.ConfigSliderItem import com.sameerasw.essentials.viewmodels.MainViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.compose.ui.platform.LocalLifecycleOwner enum class PermissionModule { HIDE_GESTURE_BAR, @@ -119,19 +127,35 @@ fun OtherCustomizationsSettingsUI( .padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp) ) { + val isShizukuGranted = viewModel.isShizukuAvailable.value && viewModel.isShizukuPermissionGranted.value + val isRootGranted = viewModel.isRootAvailable.value && viewModel.isRootPermissionGranted.value + val isShellGranted = isShizukuGranted || isRootGranted + val isAccessibilityEnabled = viewModel.isAccessibilityEnabled.value + val isUsageStatsGranted = viewModel.isUsageStatsPermissionGranted.value + val isAppDetectionGranted = if (viewModel.isUseUsageAccess.value) isUsageStatsGranted else isAccessibilityEnabled RoundedCardContainer( modifier = Modifier, spacing = 2.dp, cornerRadius = 24.dp ) { - val isShizukuGranted = viewModel.isShizukuAvailable.value && viewModel.isShizukuPermissionGranted.value - val isRootGranted = viewModel.isRootAvailable.value && viewModel.isRootPermissionGranted.value - val isShellGranted = isShizukuGranted || isRootGranted - val isAccessibilityEnabled = viewModel.isAccessibilityEnabled.value - val isUsageStatsGranted = viewModel.isUsageStatsPermissionGranted.value - val isAppDetectionGranted = if (viewModel.isUseUsageAccess.value) isUsageStatsGranted else isAccessibilityEnabled - + + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner, viewModel.isCircleToSearchGestureEnabled.value) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + viewModel.setCircleToSearchPreviewEnabled(viewModel.isCircleToSearchGestureEnabled.value) + } else if (event == Lifecycle.Event.ON_PAUSE) { + viewModel.setCircleToSearchPreviewEnabled(false) + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + viewModel.setCircleToSearchPreviewEnabled(false) + } + } + IconToggleItem( title = stringResource(R.string.feat_hide_gesture_bar_title), description = stringResource(R.string.feat_hide_gesture_bar_desc), @@ -194,6 +218,23 @@ fun OtherCustomizationsSettingsUI( iconRes = R.drawable.rounded_touch_app_24, modifier = Modifier.highlight(highlightSetting == "circle_to_search_gesture_toggle") ) + + AnimatedVisibility( + visible = viewModel.isCircleToSearchGestureEnabled.value, + enter = expandVertically(), + exit = shrinkVertically() + ) { + ConfigSliderItem( + title = stringResource(R.string.feat_circle_to_search_gesture_height_title), + value = viewModel.circleToSearchGestureHeight.floatValue, + onValueChange = { viewModel.setCircleToSearchGestureHeight(it) }, + valueRange = 24f..120f, + increment = 4f, + iconRes = R.drawable.rounded_border_bottom_24, + description = stringResource(R.string.feat_circle_to_search_gesture_height_desc), + valueFormatter = { "${it.toInt()} dp" } + ) + } } } } diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt index 8be07607d..ec3e9a6b2 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -136,6 +136,8 @@ class MainViewModel : ViewModel() { val isHideGestureBarEnabled = mutableStateOf(false) val isHideGestureBarOnLauncherEnabled = mutableStateOf(false) val isCircleToSearchGestureEnabled = mutableStateOf(false) + val circleToSearchGestureHeight = mutableFloatStateOf(48f) + val isCircleToSearchPreviewEnabled = mutableStateOf(false) @@ -508,6 +510,14 @@ class MainViewModel : ViewModel() { isCircleToSearchGestureEnabled.value = settingsRepository.getBoolean(key) } + SettingsRepository.KEY_CIRCLE_TO_SEARCH_GESTURE_HEIGHT -> { + circleToSearchGestureHeight.floatValue = settingsRepository.getFloat(key, 48f) + } + + SettingsRepository.KEY_CIRCLE_TO_SEARCH_PREVIEW_ENABLED -> { + isCircleToSearchPreviewEnabled.value = settingsRepository.getBoolean(key) + } + SettingsRepository.KEY_HIDE_GESTURE_BAR_ON_LAUNCHER_ENABLED -> { isHideGestureBarOnLauncherEnabled.value = settingsRepository.getBoolean(key) appContext?.let { updateAppDetectionService(it) } @@ -547,6 +557,8 @@ class MainViewModel : ViewModel() { isAutoAccessibilityEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_AUTO_ACCESSIBILITY_ENABLED) isHideGestureBarEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_HIDE_GESTURE_BAR_ENABLED, false) isCircleToSearchGestureEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_CIRCLE_TO_SEARCH_GESTURE_ENABLED, false) + circleToSearchGestureHeight.floatValue = settingsRepository.getFloat(SettingsRepository.KEY_CIRCLE_TO_SEARCH_GESTURE_HEIGHT, 48f) + isCircleToSearchPreviewEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_CIRCLE_TO_SEARCH_PREVIEW_ENABLED, false) isHideGestureBarOnLauncherEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_HIDE_GESTURE_BAR_ON_LAUNCHER_ENABLED, false) notificationLightingSystemMode.intValue = settingsRepository.getNotificationLightingSystemMode() if (isHideGestureBarEnabled.value) { @@ -1303,6 +1315,16 @@ class MainViewModel : ViewModel() { settingsRepository.putBoolean(SettingsRepository.KEY_CIRCLE_TO_SEARCH_GESTURE_ENABLED, enabled) } + fun setCircleToSearchGestureHeight(height: Float) { + circleToSearchGestureHeight.floatValue = height + settingsRepository.putFloat(SettingsRepository.KEY_CIRCLE_TO_SEARCH_GESTURE_HEIGHT, height) + } + + fun setCircleToSearchPreviewEnabled(enabled: Boolean) { + isCircleToSearchPreviewEnabled.value = enabled + settingsRepository.putBoolean(SettingsRepository.KEY_CIRCLE_TO_SEARCH_PREVIEW_ENABLED, enabled) + } + fun setHideGestureBarOnLauncherEnabled(enabled: Boolean, context: Context) { isHideGestureBarOnLauncherEnabled.value = enabled settingsRepository.putBoolean(SettingsRepository.KEY_HIDE_GESTURE_BAR_ON_LAUNCHER_ENABLED, enabled) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7497ff061..5048ae5d0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -358,6 +358,8 @@ Dynamically show the gesture bar only when on the home screen Circle to Search gesture Long-press the bottom area to trigger Circle to Search + Gesture zone height + Adjust the height of the touch area at the bottom Other customizations Additional system tweaks and modifications Please note that the implementation of these options may depend on the OEM and some may not be functional at all. From 81a8e5bc4b6139ebcbf6ae4cf2d9b714fd3e2ae5 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 30 Apr 2026 22:01:03 +0530 Subject: [PATCH 3/6] fix: update flashlight notification logic to correctly handle lifecycle states and update notification ID --- .../essentials/services/handlers/FlashlightHandler.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt index e7a616cb6..b28156dd2 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt @@ -42,11 +42,15 @@ class FlashlightHandler( private var flashlightJob: Job? = null private var isInternalToggle = false - private val NOTIFICATION_ID_FLASHLIGHT = 1001 + private val NOTIFICATION_ID_FLASHLIGHT = 1010 private val CHANNEL_ID_FLASHLIGHT = "flashlight_live_update" private val torchCallback = object : CameraManager.TorchCallback() { override fun onTorchModeChanged(cameraId: String, enabled: Boolean) { + if (!enabled) { + cancelFlashlightNotification() + } + val primaryId = getCameraId() if (cameraId != primaryId) return // Ignore updates from auxiliary camera IDs @@ -140,7 +144,7 @@ class FlashlightHandler( private fun updateFlashlightNotification(intensity: Int) { val prefs = service.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) - if (!prefs.getBoolean("flashlight_live_update_enabled", true)) { + if (!prefs.getBoolean("flashlight_live_update_enabled", true) || !isTorchOn) { cancelFlashlightNotification() return } From c0705047249416fed6b83ecd88e0ecbb8e0c56bf Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 30 Apr 2026 22:34:42 +0530 Subject: [PATCH 4/6] feat: add auto lock delay setting to App Lock with ConfigPickerItem component --- .../data/repository/SettingsRepository.kt | 1 + .../services/handlers/AppFlowHandler.kt | 29 +++++ .../ui/components/cards/ConfigPickerItem.kt | 114 ++++++++++++++++++ .../ui/components/menus/SegmentedMenu.kt | 5 + .../composables/configs/AppLockSettingsUI.kt | 29 +++++ .../essentials/viewmodels/MainViewModel.kt | 8 ++ app/src/main/res/values/strings.xml | 8 ++ 7 files changed, 194 insertions(+) create mode 100644 app/src/main/java/com/sameerasw/essentials/ui/components/cards/ConfigPickerItem.kt 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 c1994dfe9..049705faf 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 @@ -124,6 +124,7 @@ class SettingsRepository(private val context: Context) { const val KEY_APP_LOCK_ENABLED = "app_lock_enabled" const val KEY_APP_LOCK_SELECTED_APPS = "app_lock_selected_apps" + const val KEY_APP_LOCK_AUTO_LOCK_DELAY_INDEX = "app_lock_auto_lock_delay_index" const val KEY_USE_USAGE_ACCESS = "use_usage_access" const val KEY_FREEZE_WHEN_LOCKED_ENABLED = "freeze_when_locked_enabled" diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt index accee4f1d..b5d24fd7a 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt @@ -27,6 +27,7 @@ class AppFlowHandler( private val scope = CoroutineScope(Dispatchers.Main) private val authenticatedPackages = mutableSetOf() + private val lastLeaveTimes = mutableMapOf() // App Lock State private var lockingPackage: String? = null @@ -53,8 +54,13 @@ class AppFlowHandler( val prefs = context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) val useUsageAccess = prefs.getBoolean("use_usage_access", false) + val oldPackage = currentPackage currentPackage = packageName + if (oldPackage != null && oldPackage != packageName) { + lastLeaveTimes[oldPackage] = System.currentTimeMillis() + } + if (packageName != context.packageName && packageName != lockingPackage) { lockingPackage = null } @@ -101,6 +107,29 @@ class AppFlowHandler( val isLocked = selectedApps.find { it.packageName == packageName }?.isEnabled ?: false + if (isLocked && authenticatedPackages.contains(packageName)) { + val delayIndex = prefs.getInt("app_lock_auto_lock_delay_index", 0) + if (delayIndex > 0) { + val delayMinutes = when (delayIndex) { + 1 -> 1 + 2 -> 5 + 3 -> 10 + 4 -> 20 + 5 -> 30 + else -> 0 + } + + val lastLeaveTime = lastLeaveTimes[packageName] ?: 0L + if (lastLeaveTime > 0) { + val now = System.currentTimeMillis() + if (now - lastLeaveTime > delayMinutes * 60 * 1000L) { + authenticatedPackages.remove(packageName) + lastLeaveTimes.remove(packageName) + } + } + } + } + if (isLocked && !authenticatedPackages.contains(packageName)) { // Skip if we already requested a lock for this package very recently val now = System.currentTimeMillis() diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/ConfigPickerItem.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/ConfigPickerItem.kt new file mode 100644 index 000000000..86e1228ce --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/ConfigPickerItem.kt @@ -0,0 +1,114 @@ +package com.sameerasw.essentials.ui.components.cards + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.sameerasw.essentials.ui.components.menus.LocalDropdownMenuDismiss +import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenu +import com.sameerasw.essentials.utils.HapticUtil + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun ConfigPickerItem( + title: String, + selectedValue: String, + modifier: Modifier = Modifier, + description: String? = null, + iconRes: Int? = null, + isEnabled: Boolean = true, + onDisabledClick: (() -> Unit)? = null, + content: @Composable ColumnScope.() -> Unit +) { + val view = LocalView.current + var isMenuExpanded by remember { mutableStateOf(false) } + + ListItem( + onClick = { + if (isEnabled) { + HapticUtil.performVirtualKeyHaptic(view) + isMenuExpanded = true + } else if (onDisabledClick != null) { + HapticUtil.performVirtualKeyHaptic(view) + onDisabledClick() + } + }, + enabled = isEnabled, + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + leadingContent = if (iconRes != null && iconRes != 0) { + { + Icon( + painter = painterResource(id = iconRes), + contentDescription = title, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary + ) + } + } else null, + contentPadding = PaddingValues( + horizontal = 16.dp, + vertical = 16.dp + ), + supportingContent = if (description != null) { + { + Text( + text = description, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else null, + trailingContent = { + Box { + Surface( + onClick = { + if (isEnabled) { + HapticUtil.performVirtualKeyHaptic(view) + isMenuExpanded = true + } + }, + enabled = isEnabled, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) { + Text( + text = selectedValue, + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp) + ) + } + + SegmentedDropdownMenu( + expanded = isMenuExpanded, + onDismissRequest = { isMenuExpanded = false } + ) { + CompositionLocalProvider( + LocalDropdownMenuDismiss provides { isMenuExpanded = false } + ) { + content() + } + } + } + }, + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + content = { + Text( + text = title, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + ) +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/menus/SegmentedMenu.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/menus/SegmentedMenu.kt index d96a884c3..60c135b7a 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/menus/SegmentedMenu.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/menus/SegmentedMenu.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp +import androidx.compose.runtime.staticCompositionLocalOf import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer @Composable @@ -58,11 +59,13 @@ fun SegmentedDropdownMenuItem( ) ) { val view = androidx.compose.ui.platform.LocalView.current + val dismiss = LocalDropdownMenuDismiss.current DropdownMenuItem( text = text, onClick = { com.sameerasw.essentials.utils.HapticUtil.performUIHaptic(view) onClick() + dismiss?.invoke() }, modifier = modifier .clip(MaterialTheme.shapes.extraSmall) @@ -73,3 +76,5 @@ fun SegmentedDropdownMenuItem( colors = colors ) } + +val LocalDropdownMenuDismiss = staticCompositionLocalOf<(() -> Unit)?> { null } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/AppLockSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/AppLockSettingsUI.kt index fd55c70d7..0f2bb2174 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/AppLockSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/AppLockSettingsUI.kt @@ -20,8 +20,10 @@ import androidx.fragment.app.FragmentActivity import com.sameerasw.essentials.R import com.sameerasw.essentials.ui.components.cards.FeatureCard import com.sameerasw.essentials.ui.components.cards.IconToggleItem +import com.sameerasw.essentials.ui.components.cards.ConfigPickerItem import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer import com.sameerasw.essentials.ui.components.sheets.AppSelectionSheet +import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem import com.sameerasw.essentials.ui.modifiers.highlight import com.sameerasw.essentials.utils.BiometricHelper import com.sameerasw.essentials.viewmodels.MainViewModel @@ -43,6 +45,15 @@ fun AppLockSettingsUI( val canEnableAppLock = if (isUseUsageAccess) isUsageStatsPermissionGranted else isAccessibilityEnabled + val delayLabels = listOf( + stringResource(R.string.app_lock_auto_lock_delay_none), + stringResource(R.string.app_lock_auto_lock_delay_1min), + stringResource(R.string.app_lock_auto_lock_delay_5min), + stringResource(R.string.app_lock_auto_lock_delay_10min), + stringResource(R.string.app_lock_auto_lock_delay_20min), + stringResource(R.string.app_lock_auto_lock_delay_30min) + ) + Column( modifier = modifier .fillMaxWidth() @@ -95,6 +106,24 @@ fun AppLockSettingsUI( onClick = { isAppSelectionSheetOpen = true }, modifier = Modifier.highlight(highlightKey == "app_lock_selected_apps") ) + + ConfigPickerItem( + title = stringResource(R.string.app_lock_auto_lock_delay_title), + description = stringResource(R.string.app_lock_auto_lock_delay_desc), + iconRes = R.drawable.rounded_lock_clock_24, + isEnabled = isAppLockEnabled, + selectedValue = delayLabels[viewModel.appLockAutoLockDelayIndex.intValue], + modifier = Modifier.highlight(highlightKey == "app_lock_auto_lock_delay") + ) { + delayLabels.forEachIndexed { index, label -> + SegmentedDropdownMenuItem( + text = { Text(label) }, + onClick = { + viewModel.setAppLockAutoLockDelayIndex(index) + } + ) + } + } } Text( diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt index ec3e9a6b2..40f17deb0 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -173,6 +173,7 @@ class MainViewModel : ViewModel() { val notificationLightingSystemMode = mutableIntStateOf(0) // 0: Charging ripple, 1: Auth ripple val skipPersistentNotifications = mutableStateOf(false) val isAppLockEnabled = mutableStateOf(false) + val appLockAutoLockDelayIndex = mutableIntStateOf(0) val isUseUsageAccess = mutableStateOf(false) val isFreezeWhenLockedEnabled = mutableStateOf(false) val freezeLockDelayIndex = mutableIntStateOf(1) // Default: 1 minute @@ -967,6 +968,8 @@ class MainViewModel : ViewModel() { settingsRepository.getLong(SettingsRepository.KEY_LAST_UPDATE_CHECK_TIME) isAppLockEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_APP_LOCK_ENABLED) + appLockAutoLockDelayIndex.intValue = + settingsRepository.getInt(SettingsRepository.KEY_APP_LOCK_AUTO_LOCK_DELAY_INDEX, 0) isFreezeWhenLockedEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_FREEZE_WHEN_LOCKED_ENABLED) isFreezeDontFreezeActiveAppsEnabled.value = @@ -1437,6 +1440,11 @@ class MainViewModel : ViewModel() { updateAppDetectionService(context) } + fun setAppLockAutoLockDelayIndex(index: Int) { + appLockAutoLockDelayIndex.intValue = index + settingsRepository.putInt(SettingsRepository.KEY_APP_LOCK_AUTO_LOCK_DELAY_INDEX, index) + } + fun setUseUsageAccess(enabled: Boolean, context: Context) { isUseUsageAccess.value = enabled settingsRepository.putBoolean(SettingsRepository.KEY_USE_USAGE_ACCESS, enabled) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5048ae5d0..2c24641fd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -432,6 +432,14 @@ Screen locked security App lock Secure apps with biometrics + Auto lock delay + After leaving the app + None + 1 minute + 5 minutes + 10 minutes + 20 minutes + 30 minutes Freeze Disable rarely used apps Watermark From ff4e9d1ff1a32f3bb26d4c4fec8670a629e621f9 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 30 Apr 2026 22:44:48 +0530 Subject: [PATCH 5/6] refactor: remove legacy system lock method for screen locked security --- .../services/handlers/SecurityHandler.kt | 129 ------------------ .../tiles/ScreenOffAccessibilityService.kt | 13 +- app/src/main/res/values/strings.xml | 1 - 3 files changed, 1 insertion(+), 142 deletions(-) delete mode 100644 app/src/main/java/com/sameerasw/essentials/services/handlers/SecurityHandler.kt diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/SecurityHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/SecurityHandler.kt deleted file mode 100644 index ac7d188ff..000000000 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/SecurityHandler.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.sameerasw.essentials.services.handlers - -import android.accessibilityservice.AccessibilityService -import android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_BACK -import android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN -import android.app.KeyguardManager -import android.app.admin.DevicePolicyManager -import android.content.ComponentName -import android.content.Context -import android.provider.Settings -import android.view.accessibility.AccessibilityEvent -import android.view.accessibility.AccessibilityNodeInfo -import android.widget.Toast -import com.sameerasw.essentials.services.receivers.SecurityDeviceAdminReceiver - -class SecurityHandler( - private val service: AccessibilityService -) { - private var originalAnimationScale: Float = 1.0f - private var isScaleModified: Boolean = false - - fun onAccessibilityEvent(event: AccessibilityEvent) { - val prefs = service.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) - val isScreenLockedSecurityEnabled = - prefs.getBoolean("screen_locked_security_enabled", false) - - if (isScreenLockedSecurityEnabled) { - val keyguardManager = - service.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager - if (keyguardManager.isKeyguardLocked) { - if (event.eventType == AccessibilityEvent.TYPE_VIEW_CLICKED || event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { - val source = event.source - if (source != null) { - checkNetworkTileInteraction(source) - } - } - } - } - } - - private fun checkNetworkTileInteraction(source: AccessibilityNodeInfo) { - val keywords = listOf( - "Internet", "Mobile Data", "Wi-Fi", // English - "Daten", "WLAN", // German - "Datos", // Spanish - "Donn", // French (Donn\u00e9es) - "Cellular" // Some variants - ) - - var isNetworkTile = false - for (text in keywords) { - if (findNodeByText(source, text)) { - isNetworkTile = true - break - } - } - - if (isNetworkTile) { - setReducedAnimationScale() - service.performGlobalAction(GLOBAL_ACTION_BACK) - lockDeviceHard() - Toast.makeText( - service, - com.sameerasw.essentials.R.string.error_unlock_network_settings, - Toast.LENGTH_SHORT - ).show() - } - } - - private fun findNodeByText(node: AccessibilityNodeInfo, text: String): Boolean { - val nodes = node.findAccessibilityNodeInfosByText(text) - if (nodes.isNotEmpty()) return true - - val desc = node.contentDescription - return desc != null && desc.toString().contains(text, ignoreCase = true) - } - - private fun setReducedAnimationScale() { - if (isScaleModified) return - try { - originalAnimationScale = Settings.Global.getFloat( - service.contentResolver, - Settings.Global.ANIMATOR_DURATION_SCALE, - 1.0f - ) - Settings.Global.putFloat( - service.contentResolver, - Settings.Global.ANIMATOR_DURATION_SCALE, - 0.1f - ) - isScaleModified = true - } catch (e: Exception) { - e.printStackTrace() - } - } - - fun restoreAnimationScale() { - if (!isScaleModified) return - try { - Settings.Global.putFloat( - service.contentResolver, - Settings.Global.ANIMATOR_DURATION_SCALE, - originalAnimationScale - ) - isScaleModified = false - } catch (e: Exception) { - e.printStackTrace() - } - } - - fun lockDevice() { - service.performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN) - } - - fun lockDeviceHard() { - try { - val dpm = service.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager - val adminComponent = ComponentName(service, SecurityDeviceAdminReceiver::class.java) - if (dpm.isAdminActive(adminComponent)) { - dpm.lockNow() - } else { - service.performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN) - } - } catch (e: Exception) { - e.printStackTrace() - service.performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN) - } - } -} 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 997684edc..6f9c17abb 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 @@ -36,7 +36,6 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene private lateinit var notificationLightingHandler: NotificationLightingHandler private lateinit var buttonRemapHandler: ButtonRemapHandler private lateinit var appFlowHandler: AppFlowHandler - private lateinit var securityHandler: SecurityHandler private lateinit var ambientGlanceHandler: AmbientGlanceHandler private lateinit var aodForceTurnOffHandler: AodForceTurnOffHandler private lateinit var omniGestureOverlayHandler: OmniGestureOverlayHandler @@ -70,7 +69,6 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene notificationLightingHandler = NotificationLightingHandler(this) buttonRemapHandler = ButtonRemapHandler(this, flashlightHandler) appFlowHandler = AppFlowHandler(this, this) - securityHandler = SecurityHandler(this) ambientGlanceHandler = AmbientGlanceHandler(this) aodForceTurnOffHandler = AodForceTurnOffHandler(this) omniGestureOverlayHandler = OmniGestureOverlayHandler(this) @@ -99,7 +97,6 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene } Intent.ACTION_USER_PRESENT -> { - securityHandler.restoreAnimationScale() } InputEventListenerService.ACTION_VOLUME_LONG_PRESSED -> { @@ -180,7 +177,6 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene } flashlightHandler.unregister() sensorManager.unregisterListener(this) - securityHandler.restoreAnimationScale() notificationLightingHandler.removeOverlay() ambientGlanceHandler.removeOverlay() aodForceTurnOffHandler.removeOverlay() @@ -199,13 +195,6 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene val packageName = event.packageName?.toString() ?: return appFlowHandler.onPackageChanged(packageName) } - - // Bypass security scanning for camera apps to avoid performance interference - if (appFlowHandler.isCameraApp()) { - return - } - - securityHandler.onAccessibilityEvent(event) } override fun onInterrupt() {} @@ -291,7 +280,7 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene val vibrator = getSystemService(VIBRATOR_SERVICE) as? Vibrator vibrator?.let { performHapticFeedback(it, hapticType) } } - securityHandler.lockDevice() + performGlobalAction(AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN) } "SHOW_NOTIFICATION_LIGHTING" -> notificationLightingHandler.handleIntent(intent) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2c24641fd..24fdb2c6a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1071,7 +1071,6 @@ Flashlight Brightness - Unlock phone to change network settings Developed by %1$s\nwith ❤\uFE0F from \uD83C\uDDF1\uD83C\uDDF0 From 34011c23b226925bcc2fd2f54ad39247cc0bed8f Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 30 Apr 2026 22:55:52 +0530 Subject: [PATCH 6/6] fix: enable sheet dismissal by updating state in LinkPickerAdapter --- .../essentials/ui/components/linkActions/LinkPickerAdapter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/linkActions/LinkPickerAdapter.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/linkActions/LinkPickerAdapter.kt index 17097152a..619b12f68 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/linkActions/LinkPickerAdapter.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/linkActions/LinkPickerAdapter.kt @@ -365,7 +365,7 @@ fun LinkPickerScreen( } ModalBottomSheet( - onDismissRequest = { }, + onDismissRequest = { showEditSheet = false }, sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), containerColor = MaterialTheme.colorScheme.surfaceContainer ) {