diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 77463f524..132d3fe10 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -888,6 +888,19 @@ android:value="android.service.quicksettings.CATEGORY_DISPLAY" /> + + + + + + + !isWriteSecureSettingsEnabled "Notification lighting" -> !isOverlayPermissionGranted || !isNotificationLightingAccessibilityEnabled || !isNotificationListenerEnabled "Button remap" -> !isAccessibilityEnabled + "Pocket mode" -> !isAccessibilityEnabled "Dynamic night light" -> (if (viewModel.isUseUsageAccess.value) !viewModel.isUsageStatsPermissionGranted.value else !isAccessibilityEnabled) || !isWriteSecureSettingsEnabled "Snooze system notifications" -> !isNotificationListenerEnabled "Screen locked security" -> !isAccessibilityEnabled || !isWriteSecureSettingsEnabled || !viewModel.isDeviceAdminEnabled.value @@ -674,6 +676,14 @@ class FeatureSettingsActivity : AppCompatActivity() { ) } + "Pocket mode" -> { + PocketModeSettingsUI( + viewModel = viewModel, + modifier = Modifier.padding(top = 16.dp), + highlightSetting = highlightSetting + ) + } + "App lock" -> { AppLockSettingsUI( 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 61bd2c3ad..368dbda14 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 @@ -120,6 +120,7 @@ class SettingsRepository(private val context: Context) { const val KEY_FLASHLIGHT_PULSE_FACEDOWN_ONLY = "flashlight_pulse_facedown_only" const val KEY_FLASHLIGHT_PULSE_MAX_INTENSITY = "flashlight_pulse_max_intensity" const val KEY_FLASHLIGHT_PULSE_DISABLE_ON_DND = "flashlight_pulse_disable_on_dnd" + const val KEY_FLASHLIGHT_POCKET_TURN_OFF_ENABLED = "flashlight_pocket_turn_off_enabled" const val KEY_SCREEN_LOCKED_SECURITY_ENABLED = "screen_locked_security_enabled" const val KEY_HIDE_SYSTEM_ICONS = "hide_system_icons" @@ -279,6 +280,11 @@ class SettingsRepository(private val context: Context) { const val KEY_LOCK_SCREEN_CLOCK_SELECTED_COLOR_ID = "lock_screen_clock_selected_color_id" const val KEY_LOCK_SCREEN_CLOCK_SEED_COLOR = "lock_screen_clock_seed_color" const val KEY_RECENT_SEARCHES = "recent_searches" + const val KEY_POCKET_MODE_ENABLED = "pocket_mode_enabled" + const val KEY_POCKET_MODE_USE_LIGHT_SENSOR = "pocket_mode_use_light_sensor" + const val KEY_POCKET_MODE_EXCLUDED_APPS = "pocket_mode_excluded_apps" + const val KEY_POCKET_MODE_TRIGGER_DELAY = "pocket_mode_trigger_delay" + const val KEY_POCKET_MODE_LOCK_SCREEN_ONLY = "pocket_mode_lock_screen_only" } // Observe changes @@ -538,6 +544,12 @@ class SettingsRepository(private val context: Context) { fun updateNotificationGlanceAppSelection(packageName: String, enabled: Boolean) = updateAppSelection(KEY_NOTIFICATION_GLANCE_SELECTED_APPS, packageName, enabled) + fun loadPocketModeExcludedApps() = loadAppSelection(KEY_POCKET_MODE_EXCLUDED_APPS) + fun savePocketModeExcludedApps(apps: List) = + saveAppSelection(KEY_POCKET_MODE_EXCLUDED_APPS, apps) + fun updatePocketModeExcludedAppSelection(packageName: String, enabled: Boolean) = + updateAppSelection(KEY_POCKET_MODE_EXCLUDED_APPS, packageName, enabled) + fun loadShutUpConfigs(): List { val json = prefs.getString(KEY_SHUT_UP_SELECTED_APPS, null) return if (json != null) { 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 8b1754829..a67c13b5f 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 @@ -745,6 +745,12 @@ object FeatureRegistry { R.string.search_remap_flashlight_desc, "flashlight_toggle", R.array.keywords_flashlight + ), + SearchSetting( + R.string.search_flashlight_pocket_title, + R.string.search_flashlight_pocket_desc, + "flashlight_options", + R.array.keywords_pocket_detection ) ), parentFeatureId = "Input", @@ -912,6 +918,22 @@ object FeatureRegistry { override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {} }, + object : Feature( + id = "Pocket mode", + title = R.string.feat_pocket_mode_title, + iconRes = R.drawable.ic_pocket_mode, + category = R.string.cat_display, + description = R.string.feat_pocket_mode_desc, + permissionKeys = listOf("ACCESSIBILITY"), + aboutDescription = R.string.feat_pocket_mode_desc, + parentFeatureId = "Display", + hasMoreSettings = true, + ) { + override fun isEnabled(viewModel: MainViewModel) = viewModel.isPocketModeEnabled.value + + override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) = + viewModel.setPocketModeEnabled(enabled) + }, object : Feature( id = "Location reached", title = R.string.feat_location_reached_title, 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 ada731651..a0b77fea8 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 @@ -44,6 +44,8 @@ class FlashlightHandler( var isTorchOn = false private set + var isProximityBlocked = false + private var primaryCameraId: String? = null private var currentIntensityLevel: Int = 1 private var flashlightJob: Job? = null @@ -64,6 +66,9 @@ class FlashlightHandler( super.onTorchModeChanged(cameraId, enabled) isTorchOn = enabled + val screenOffService = service as? com.sameerasw.essentials.services.tiles.ScreenOffAccessibilityService + screenOffService?.updateFlashlightProximityRegistration(enabled) + val prefs = service.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) val isGlobalEnabled = prefs.getBoolean("flashlight_global_enabled", false) val lastIntensity = prefs.getInt("flashlight_last_intensity", 1) diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/PocketModeHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/PocketModeHandler.kt new file mode 100644 index 000000000..1a02c200f --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/PocketModeHandler.kt @@ -0,0 +1,248 @@ +package com.sameerasw.essentials.services.handlers + +import android.accessibilityservice.AccessibilityService +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.View +import android.view.WindowManager +import android.widget.FrameLayout +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.setViewTreeLifecycleOwner +import androidx.lifecycle.setViewTreeViewModelStoreOwner +import androidx.savedstate.SavedStateRegistry +import androidx.savedstate.SavedStateRegistryController +import androidx.savedstate.SavedStateRegistryOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import com.sameerasw.essentials.R +import com.sameerasw.essentials.utils.OverlayHelper + +class PocketModeHandler(private val service: AccessibilityService) { + + private var windowManager: WindowManager? = null + private var overlayView: View? = null + private var lifecycleOwner: OverlayLifecycleOwner? = null + + var isBypassed = false + var isOverlayVisible = false + + private val handler = Handler(Looper.getMainLooper()) + private var isPending = false + private val showOverlayRunnable = Runnable { + isPending = false + if (!isBypassed) { + showOverlay() + } + } + private val screenOffRunnable = Runnable { + if (isOverlayVisible) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN) + } + } + } + + fun showOverlay() { + if (isOverlayVisible || overlayView != null) return + + Log.d("PocketModeHandler", "Showing pocket mode overlay") + windowManager = service.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val context = service + + val owner = OverlayLifecycleOwner() + lifecycleOwner = owner + + val frameLayout = FrameLayout(context) + owner.onCreate() + frameLayout.setViewTreeLifecycleOwner(owner) + frameLayout.setViewTreeSavedStateRegistryOwner(owner) + frameLayout.setViewTreeViewModelStoreOwner(owner) + + val composeView = ComposeView(context).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindow) + setContent { + PocketModeOverlayContent() + } + } + frameLayout.addView(composeView) + + overlayView = frameLayout + + val params = OverlayHelper.createOverlayLayoutParams( + overlayType = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY, + flags = 0, + isTouchable = true + ) + + try { + windowManager?.addView(frameLayout, params) + isOverlayVisible = true + handler.postDelayed(screenOffRunnable, 5000L) + } catch (e: Exception) { + Log.e("PocketModeHandler", "Error adding pocket overlay", e) + removeOverlay() + } + } + + fun removeOverlay() { + handler.removeCallbacks(screenOffRunnable) + if (overlayView != null) { + Log.d("PocketModeHandler", "Removing pocket mode overlay") + try { + windowManager?.removeView(overlayView) + } catch (e: Exception) { + Log.e("PocketModeHandler", "Error removing pocket overlay view", e) + } + overlayView = null + } + lifecycleOwner?.onDestroy() + lifecycleOwner = null + isOverlayVisible = false + isBypassed = false + } + + fun onProximityChanged(isBlocked: Boolean, isLightDark: Boolean, useLightSensor: Boolean, triggerDelayMs: Long = 3000L) { + val shouldShow = isBlocked && (!useLightSensor || isLightDark) + if (shouldShow) { + if (!isBypassed && !isOverlayVisible && !isPending) { + isPending = true + handler.postDelayed(showOverlayRunnable, triggerDelayMs) + } + } else { + if (isPending) { + handler.removeCallbacks(showOverlayRunnable) + isPending = false + } + removeOverlay() + } + } + + fun onScreenOff() { + handler.removeCallbacks(showOverlayRunnable) + handler.removeCallbacks(screenOffRunnable) + isPending = false + removeOverlay() + isBypassed = false + } + + private class OverlayLifecycleOwner : LifecycleOwner, SavedStateRegistryOwner, ViewModelStoreOwner { + private val lifecycleRegistry = LifecycleRegistry(this) + private val savedStateRegistryController = SavedStateRegistryController.create(this) + private val store = ViewModelStore() + + override val lifecycle: Lifecycle = lifecycleRegistry + override val savedStateRegistry: SavedStateRegistry = savedStateRegistryController.savedStateRegistry + override val viewModelStore: ViewModelStore = store + + fun onCreate() { + savedStateRegistryController.performRestore(null) + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) + } + + fun onDestroy() { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + store.clear() + } + } + + @Composable + private fun PocketModeOverlayContent() { + val infiniteTransition = rememberInfiniteTransition(label = "pulse") + val scale by infiniteTransition.animateFloat( + initialValue = 0.8f, + targetValue = 1.2f, + animationSpec = infiniteRepeatable( + animation = tween(1000, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), + label = "scale" + ) + val alpha by infiniteTransition.animateFloat( + initialValue = 0.4f, + targetValue = 0.9f, + animationSpec = infiniteRepeatable( + animation = tween(1000, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), + label = "alpha" + ) + + val context = LocalContext.current + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black) + ) { + // Pulsing circle at top center (near front camera/proximity sensor) + Box( + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = 140.dp) + .size(52.dp) + .graphicsLayer( + scaleX = scale, + scaleY = scale, + alpha = alpha + ) + .background(Color.White.copy(alpha = 0.3f), shape = CircleShape) + ) + + // Center details + Column( + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = context.getString(R.string.pocket_mode_active), + color = Color.White, + fontSize = 24.sp, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = context.getString(R.string.pocket_mode_dismiss_hint), + color = Color.LightGray, + fontSize = 14.sp + ) + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/PocketModeTileService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/PocketModeTileService.kt new file mode 100644 index 000000000..8ced036a8 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/PocketModeTileService.kt @@ -0,0 +1,34 @@ +package com.sameerasw.essentials.services.tiles + +import android.graphics.drawable.Icon +import android.service.quicksettings.Tile +import com.sameerasw.essentials.R +import com.sameerasw.essentials.utils.PermissionUtils + +class PocketModeTileService : BaseTileService() { + + override fun getTileLabel(): String = getString(R.string.feat_pocket_mode_title) + + override fun getTileSubtitle(): String { + return if (getTileState() == Tile.STATE_ACTIVE) "Enabled" else "Disabled" + } + + override fun hasFeaturePermission(): Boolean { + return PermissionUtils.isAccessibilityServiceEnabled(this) + } + + override fun getTileIcon(): Icon = + Icon.createWithResource(this, R.drawable.ic_pocket_mode) + + override fun getTileState(): Int { + val enabled = getSharedPreferences("essentials_prefs", MODE_PRIVATE) + .getBoolean("pocket_mode_enabled", false) + return if (enabled) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE + } + + override fun onTileClick() { + val prefs = getSharedPreferences("essentials_prefs", MODE_PRIVATE) + val isEnabled = prefs.getBoolean("pocket_mode_enabled", false) + prefs.edit().putBoolean("pocket_mode_enabled", !isEnabled).apply() + } +} 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 42d9de18b..cc30768ab 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 @@ -14,14 +14,17 @@ import android.media.session.MediaSessionManager import android.os.Handler import android.os.Looper import android.os.Vibrator +import android.app.KeyguardManager import android.view.KeyEvent import android.view.accessibility.AccessibilityEvent import com.sameerasw.essentials.data.repository.SettingsRepository import com.sameerasw.essentials.domain.HapticFeedbackType +import com.sameerasw.essentials.domain.model.AppSelection import com.sameerasw.essentials.services.InputEventListenerService import com.sameerasw.essentials.services.NotificationListener import com.sameerasw.essentials.services.handlers.AmbientGlanceHandler import com.sameerasw.essentials.services.handlers.AodForceTurnOffHandler +import com.sameerasw.essentials.services.handlers.PocketModeHandler import com.sameerasw.essentials.services.handlers.AppFlowHandler import com.sameerasw.essentials.services.handlers.ButtonRemapHandler import com.sameerasw.essentials.services.handlers.FlashlightHandler @@ -36,10 +39,13 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -class ScreenOffAccessibilityService : AccessibilityService() { +class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListener { private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + private val sensorManager by lazy { getSystemService(SENSOR_SERVICE) as SensorManager } + private var proximitySensor: Sensor? = null + // Handlers private lateinit var flashlightHandler: FlashlightHandler private lateinit var notificationLightingHandler: NotificationLightingHandler @@ -49,6 +55,69 @@ class ScreenOffAccessibilityService : AccessibilityService() { private lateinit var aodForceTurnOffHandler: AodForceTurnOffHandler private lateinit var omniGestureOverlayHandler: OmniGestureOverlayHandler private lateinit var statusBarIconHandler: StatusBarIconHandler + private lateinit var pocketModeHandler: PocketModeHandler + + private var lightSensor: Sensor? = null + private var lightSensorLux: Float = 100f + private var pocketModeExcludedAppsSet: Set = emptySet() + private val appCategoryCache = mutableMapOf() + private val keyguardManager by lazy { getSystemService(KEYGUARD_SERVICE) as KeyguardManager } + + private var isScreenOn = true + private var isLightSensorRegistered = false + private var isProximityRegisteredForPocket = false + private var isProximityRegistered = false + + private fun updatePocketModeExcludedAppsSet() { + val prefs = getSharedPreferences("essentials_prefs", MODE_PRIVATE) + val json = prefs.getString("pocket_mode_excluded_apps", null) + pocketModeExcludedAppsSet = if (json != null) { + try { + val gson = com.google.gson.GsonBuilder().create() + gson.fromJson(json, Array::class.java) + .filter { it.isEnabled } + .map { it.packageName } + .toSet() + } catch (e: Exception) { + emptySet() + } + } else { + emptySet() + } + } + + private fun isGameOrVideoApp(packageName: String): Boolean { + return appCategoryCache.getOrPut(packageName) { + try { + val info = packageManager.getApplicationInfo(packageName, 0) + val isLegacyGame = (info.flags and android.content.pm.ApplicationInfo.FLAG_IS_GAME) != 0 + val isCategoryMatch = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + val category = info.category + category == android.content.pm.ApplicationInfo.CATEGORY_GAME || + category == android.content.pm.ApplicationInfo.CATEGORY_VIDEO + } else { + false + } + isLegacyGame || isCategoryMatch + } catch (e: Exception) { + false + } + } + } + + private fun hasActiveMediaSession(packageName: String): Boolean { + return try { + val msm = getSystemService(MEDIA_SESSION_SERVICE) as MediaSessionManager + val componentName = android.content.ComponentName(this, NotificationListener::class.java) + val sessions = msm.getActiveSessions(componentName) + sessions.any { + it.packageName == packageName && + it.playbackState?.state == android.media.session.PlaybackState.STATE_PLAYING + } + } catch (e: Exception) { + false + } + } private var screenReceiver: BroadcastReceiver? = null @@ -59,6 +128,59 @@ class ScreenOffAccessibilityService : AccessibilityService() { FreezeManager.freezeAll(this) } + // Pocket Detection + private val pocketFlashlightHandler = Handler(Looper.getMainLooper()) + private val pocketFlashlightRunnable = Runnable { + val prefs = getSharedPreferences("essentials_prefs", MODE_PRIVATE) + val pocketTurnOffEnabled = prefs.getBoolean("flashlight_pocket_turn_off_enabled", false) + // Re-check at fire time — guards against external torch-off between scheduling and firing + if (pocketTurnOffEnabled && flashlightHandler.isProximityBlocked && flashlightHandler.isTorchOn) { + flashlightHandler.toggleFlashlight() + } + } + + private fun schedulePocketFlashlightTurnOff() { + pocketFlashlightHandler.removeCallbacks(pocketFlashlightRunnable) + pocketFlashlightHandler.postDelayed(pocketFlashlightRunnable, 1500L) + } + + private fun cancelPocketFlashlightTurnOff() { + pocketFlashlightHandler.removeCallbacks(pocketFlashlightRunnable) + } + + private fun updateProximitySensorRegistration() { + val prefs = getSharedPreferences("essentials_prefs", MODE_PRIVATE) + val pocketTurnOffEnabled = prefs.getBoolean("flashlight_pocket_turn_off_enabled", false) + val flashlightNeedsProximity = pocketTurnOffEnabled && flashlightHandler.isTorchOn + + val shouldRegister = isProximityRegisteredForPocket || flashlightNeedsProximity + + if (shouldRegister) { + if (!isProximityRegistered) { + if (proximitySensor == null) { + proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY) + } + proximitySensor?.let { + sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_NORMAL) + isProximityRegistered = true + android.util.Log.d("ScreenOffService", "Registered proximity sensor listener") + } + } + } else { + if (isProximityRegistered) { + proximitySensor?.let { + sensorManager.unregisterListener(this, it) + } + isProximityRegistered = false + android.util.Log.d("ScreenOffService", "Unregistered proximity sensor listener") + } + } + } + + fun updateFlashlightProximityRegistration(register: Boolean) { + updateProximitySensorRegistration() + } + private val preferenceChangeListener = android.content.SharedPreferences.OnSharedPreferenceChangeListener { _, key -> if (key == "circle_to_search_gesture_enabled" || @@ -71,6 +193,10 @@ class ScreenOffAccessibilityService : AccessibilityService() { ) == true ) { statusBarIconHandler.updateAll() + } else if (key == "pocket_mode_enabled" || key == "pocket_mode_use_light_sensor") { + updatePocketModeSensors() + } else if (key == "pocket_mode_excluded_apps") { + updatePocketModeExcludedAppsSet() } } @@ -87,6 +213,7 @@ class ScreenOffAccessibilityService : AccessibilityService() { aodForceTurnOffHandler = AodForceTurnOffHandler(this) omniGestureOverlayHandler = OmniGestureOverlayHandler(this) statusBarIconHandler = StatusBarIconHandler(this) + pocketModeHandler = PocketModeHandler(this) flashlightHandler.register() statusBarIconHandler.register() @@ -96,23 +223,32 @@ class ScreenOffAccessibilityService : AccessibilityService() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { Intent.ACTION_SCREEN_ON -> { + isScreenOn = true notificationLightingHandler.onScreenOn() ambientGlanceHandler.dismissImmediately() aodForceTurnOffHandler.removeOverlay() freezeHandler.removeCallbacks(freezeRunnable) stopInputEventListener() updateOmniOverlay() + updatePocketModeSensors() } Intent.ACTION_SCREEN_OFF -> { + isScreenOn = false appFlowHandler.clearAuthenticated() scheduleFreeze() startInputEventListenerIfEnabled() ambientGlanceHandler.checkAndShowOnScreenOff() omniGestureOverlayHandler.updateOverlay(false) // Always hide when screen is off + pocketModeHandler.onScreenOff() + updatePocketModeSensors() } Intent.ACTION_USER_PRESENT -> { + val prefs = getSharedPreferences("essentials_prefs", MODE_PRIVATE) + if (prefs.getBoolean("pocket_mode_lock_screen_only", false)) { + pocketModeHandler.onScreenOff() // cancel pending timer + remove overlay + } } InputEventListenerService.ACTION_VOLUME_LONG_PRESSED -> { @@ -157,6 +293,12 @@ class ScreenOffAccessibilityService : AccessibilityService() { getSharedPreferences("essentials_prefs", MODE_PRIVATE) .registerOnSharedPreferenceChangeListener(preferenceChangeListener) + + val powerManager = getSystemService(POWER_SERVICE) as? android.os.PowerManager + isScreenOn = powerManager?.isInteractive ?: true + + updatePocketModeExcludedAppsSet() + updatePocketModeSensors() } private fun scheduleFreeze() { @@ -209,9 +351,23 @@ class ScreenOffAccessibilityService : AccessibilityService() { notificationLightingHandler.removeOverlay() ambientGlanceHandler.removeOverlay() aodForceTurnOffHandler.removeOverlay() + pocketModeHandler.removeOverlay() omniGestureOverlayHandler.removeOverlay() statusBarIconHandler.unregister() stopInputEventListener() + cancelPocketFlashlightTurnOff() + if (isProximityRegistered) { + proximitySensor?.let { + sensorManager.unregisterListener(this, it) + } + isProximityRegistered = false + } + if (isLightSensorRegistered) { + lightSensor?.let { + sensorManager.unregisterListener(this, it) + } + isLightSensorRegistered = false + } serviceScope.cancel() getSharedPreferences("essentials_prefs", MODE_PRIVATE) .unregisterOnSharedPreferenceChangeListener(preferenceChangeListener) @@ -230,7 +386,108 @@ class ScreenOffAccessibilityService : AccessibilityService() { override fun onInterrupt() {} + private fun updatePocketModeSensors() { + val prefs = getSharedPreferences("essentials_prefs", MODE_PRIVATE) + val pocketModeEnabled = prefs.getBoolean("pocket_mode_enabled", false) + val useLightSensor = prefs.getBoolean("pocket_mode_use_light_sensor", false) + + val shouldRegisterLight = pocketModeEnabled && useLightSensor && isScreenOn + val shouldRegisterProximity = pocketModeEnabled && isScreenOn + + if (shouldRegisterLight) { + if (lightSensor == null) { + lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT) + } + if (lightSensor != null && !isLightSensorRegistered) { + sensorManager.registerListener(this, lightSensor, SensorManager.SENSOR_DELAY_NORMAL) + isLightSensorRegistered = true + android.util.Log.d("ScreenOffService", "Registered light sensor for pocket mode") + } + } else { + if (isLightSensorRegistered) { + lightSensor?.let { + sensorManager.unregisterListener(this, it) + } + isLightSensorRegistered = false + android.util.Log.d("ScreenOffService", "Unregistered light sensor for pocket mode") + } + lightSensorLux = 100f + } + + if (shouldRegisterProximity) { + isProximityRegisteredForPocket = true + } else { + isProximityRegisteredForPocket = false + } + + updateProximitySensorRegistration() + } + + override fun onSensorChanged(event: SensorEvent?) { + if (event == null) return + if (event.sensor.type == Sensor.TYPE_LIGHT) { + lightSensorLux = event.values[0] + val prefs = getSharedPreferences("essentials_prefs", MODE_PRIVATE) + val pocketModeEnabled = prefs.getBoolean("pocket_mode_enabled", false) + val useLightSensor = prefs.getBoolean("pocket_mode_use_light_sensor", false) + val triggerDelayMs = (prefs.getFloat("pocket_mode_trigger_delay", 3f) * 1000).toLong() + val lockScreenOnly = prefs.getBoolean("pocket_mode_lock_screen_only", false) + if (pocketModeEnabled && !pocketModeHandler.isBypassed) { + val currentApp = appFlowHandler.currentPackage + val shouldBypass = (currentApp != null && ( + pocketModeExcludedAppsSet.contains(currentApp) || + isGameOrVideoApp(currentApp) || + hasActiveMediaSession(currentApp) + )) || (lockScreenOnly && !keyguardManager.isKeyguardLocked) + if (!shouldBypass) { + pocketModeHandler.onProximityChanged( + isBlocked = flashlightHandler.isProximityBlocked, + isLightDark = lightSensorLux <= 3f, + useLightSensor = useLightSensor, + triggerDelayMs = triggerDelayMs + ) + } + } + } else if (event.sensor.type == Sensor.TYPE_PROXIMITY) { + val distance = event.values[0] + val maxRange = event.sensor.maximumRange + val isBlocked = distance < maxRange && distance < 5f + flashlightHandler.isProximityBlocked = isBlocked + + val prefs = getSharedPreferences("essentials_prefs", MODE_PRIVATE) + val pocketTurnOffEnabled = prefs.getBoolean("flashlight_pocket_turn_off_enabled", false) + + if (pocketTurnOffEnabled && isBlocked && flashlightHandler.isTorchOn) { + schedulePocketFlashlightTurnOff() + } else { + cancelPocketFlashlightTurnOff() + } + + val pocketModeEnabled = prefs.getBoolean("pocket_mode_enabled", false) + val useLightSensor = prefs.getBoolean("pocket_mode_use_light_sensor", false) + val triggerDelayMs = (prefs.getFloat("pocket_mode_trigger_delay", 3f) * 1000).toLong() + val lockScreenOnly = prefs.getBoolean("pocket_mode_lock_screen_only", false) + if (pocketModeEnabled && !pocketModeHandler.isBypassed) { + val currentApp = appFlowHandler.currentPackage + val shouldBypass = (currentApp != null && ( + pocketModeExcludedAppsSet.contains(currentApp) || + isGameOrVideoApp(currentApp) || + hasActiveMediaSession(currentApp) + )) || (lockScreenOnly && !keyguardManager.isKeyguardLocked) + if (!shouldBypass) { + pocketModeHandler.onProximityChanged( + isBlocked = isBlocked, + isLightDark = lightSensorLux <= 3f, + useLightSensor = useLightSensor, + triggerDelayMs = triggerDelayMs + ) + } + } + } + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} override fun onConfigurationChanged(newConfig: android.content.res.Configuration) { super.onConfigurationChanged(newConfig) @@ -243,6 +500,13 @@ class ScreenOffAccessibilityService : AccessibilityService() { keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN if (isVolumeKey) { + if (pocketModeHandler.isOverlayVisible) { + if (event.action == KeyEvent.ACTION_DOWN) { + pocketModeHandler.isBypassed = true + pocketModeHandler.removeOverlay() + } + return true + } // Bypass logic for Camera apps to resolve conflicts with shutter/zoom functions val foregroundPackage = rootInActiveWindow?.packageName?.toString() ?: appFlowHandler.currentPackage diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt index fe30975c6..436219693 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt @@ -792,7 +792,6 @@ class AutomationEditorActivity : ComponentActivity() { } ) } - // Bottom Actions Row( modifier = Modifier diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ButtonRemapSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ButtonRemapSettingsUI.kt index 83a97c10a..d5661efe5 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ButtonRemapSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ButtonRemapSettingsUI.kt @@ -465,6 +465,7 @@ fun ButtonRemapSettingsUI( isChecked = viewModel.isFlashlightAlwaysTurnOffEnabled.value, onCheckedChange = { viewModel.setFlashlightAlwaysTurnOffEnabled(it, context) } ) + } Button( diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/FlashlightSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/FlashlightSettingsUI.kt index cfc2a75b6..d1763d987 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/FlashlightSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/FlashlightSettingsUI.kt @@ -65,6 +65,14 @@ fun FlashlightSettingsUI( onCheckedChange = { viewModel.setFlashlightLiveUpdateEnabled(it, context) }, modifier = Modifier.highlight(highlightSetting == "flashlight_live_update") ) + IconToggleItem( + iconRes = R.drawable.rounded_front_hand_24, + title = stringResource(R.string.flashlight_pocket_title), + description = stringResource(R.string.flashlight_pocket_desc), + isChecked = viewModel.isFlashlightPocketTurnOffEnabled.value, + onCheckedChange = { viewModel.setFlashlightPocketTurnOffEnabled(it, context) } + ) + } } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/PocketModeSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/PocketModeSettingsUI.kt new file mode 100644 index 000000000..b687e477f --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/PocketModeSettingsUI.kt @@ -0,0 +1,174 @@ +package com.sameerasw.essentials.ui.composables.configs + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.sameerasw.essentials.R +import com.sameerasw.essentials.ui.components.cards.IconToggleItem +import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer +import com.sameerasw.essentials.ui.modifiers.highlight +import com.sameerasw.essentials.viewmodels.MainViewModel +import androidx.compose.material3.Button +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import com.sameerasw.essentials.ui.components.sheets.AppSelectionSheet +import com.sameerasw.essentials.ui.components.sliders.ConfigSliderItem +import com.sameerasw.essentials.utils.HapticUtil + +@Composable +fun PocketModeSettingsUI( + viewModel: MainViewModel, + modifier: Modifier = Modifier, + highlightSetting: String? = null +) { + val context = LocalContext.current + val view = LocalView.current + var showAppSelectionSheet by remember { mutableStateOf(false) } + + Column( + modifier = modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = stringResource(R.string.feat_pocket_mode_title), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 8.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + RoundedCardContainer( + modifier = Modifier, + spacing = 2.dp, + cornerRadius = 24.dp + ) { + IconToggleItem( + title = stringResource(R.string.feat_pocket_mode_title), + description = stringResource(R.string.feat_pocket_mode_desc), + isChecked = viewModel.isPocketModeEnabled.value, + onCheckedChange = { isChecked -> + viewModel.setPocketModeEnabled(isChecked) + }, + enabled = true, + iconRes = R.drawable.ic_pocket_mode, + modifier = Modifier.highlight(highlightSetting == "pocket_mode_toggle") + ) + + IconToggleItem( + title = stringResource(R.string.pocket_mode_use_light_title), + description = stringResource(R.string.pocket_mode_use_light_desc), + isChecked = viewModel.isPocketModeUseLightSensor.value, + onCheckedChange = { isChecked -> + viewModel.setPocketModeUseLightSensor(isChecked) + }, + enabled = viewModel.isPocketModeEnabled.value, + iconRes = R.drawable.ic_light_sensor, + modifier = Modifier.highlight(highlightSetting == "pocket_mode_use_light_sensor") + ) + + ConfigSliderItem( + title = stringResource(R.string.pocket_mode_trigger_delay_title), + value = viewModel.pocketModeTriggerDelay.floatValue, + onValueChange = { viewModel.setPocketModeTriggerDelay(it) }, + valueRange = 0f..30f, + steps = 29, + increment = 1f, + valueFormatter = { "${it.toInt()}s" }, + enabled = viewModel.isPocketModeEnabled.value, + iconRes = R.drawable.rounded_timer_24 + ) + + IconToggleItem( + title = stringResource(R.string.pocket_mode_lock_screen_only_title), + description = stringResource(R.string.pocket_mode_lock_screen_only_desc), + isChecked = viewModel.isPocketModeLockScreenOnly.value, + onCheckedChange = { viewModel.setPocketModeLockScreenOnly(it) }, + enabled = viewModel.isPocketModeEnabled.value, + iconRes = R.drawable.rounded_lock_24 + ) + } + + Spacer(modifier = Modifier.size(16.dp)) + + Text( + text = stringResource(R.string.pocket_mode_exclude_apps_title), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 8.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Button( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + showAppSelectionSheet = true + }, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.action_select_apps)) + } + + Text( + text = stringResource(R.string.pocket_mode_exclude_apps_desc), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + if (showAppSelectionSheet) { + AppSelectionSheet( + onDismissRequest = { showAppSelectionSheet = false }, + onLoadApps = { viewModel.loadPocketModeExcludedApps(it) }, + onSaveApps = { ctx, apps -> + viewModel.savePocketModeExcludedApps(ctx, apps) + }, + onAppToggle = { ctx, pkg, enabled -> + viewModel.updatePocketModeExcludedAppEnabled(ctx, pkg, enabled) + }, + context = context + ) + } + + androidx.compose.foundation.layout.Spacer(modifier = Modifier.padding(8.dp)) + + RoundedCardContainer( + modifier = Modifier.fillMaxWidth(), + containerColor = MaterialTheme.colorScheme.errorContainer, + cornerRadius = 24.dp + ) { + androidx.compose.foundation.layout.Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically + ) { + androidx.compose.material3.Icon( + painter = androidx.compose.ui.res.painterResource(id = R.drawable.rounded_info_24), + contentDescription = "Warning", + tint = MaterialTheme.colorScheme.onErrorContainer, + modifier = Modifier.size(24.dp) + ) + androidx.compose.foundation.layout.Spacer(modifier = Modifier.width(16.dp)) + Text( + text = stringResource(R.string.pocket_mode_warning), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onErrorContainer + ) + } + } + } +} 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 d7247d334..7b9e03ad3 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -118,6 +118,7 @@ class MainViewModel : ViewModel() { val isFlashlightPulseUseLightingApps = mutableStateOf(true) val flashlightPulseMaxIntensity = mutableFloatStateOf(0.5f) val isFlashlightPulseDisableOnDnd = mutableStateOf(true) + val isFlashlightPocketTurnOffEnabled = mutableStateOf(false) val isLocationPermissionGranted = mutableStateOf(false) val isBackgroundLocationPermissionGranted = mutableStateOf(false) val isFullScreenIntentPermissionGranted = mutableStateOf(false) @@ -133,6 +134,10 @@ class MainViewModel : ViewModel() { val isAodEnabled = mutableStateOf(false) val isNotificationGlanceEnabled = mutableStateOf(false) val isAodForceTurnOffEnabled = mutableStateOf(false) + val isPocketModeEnabled = mutableStateOf(false) + val isPocketModeUseLightSensor = mutableStateOf(false) + val pocketModeTriggerDelay = mutableFloatStateOf(3f) // seconds + val isPocketModeLockScreenOnly = mutableStateOf(false) val isAutoAccessibilityEnabled = mutableStateOf(false) val isNotificationGlanceSameAsLightingEnabled = mutableStateOf(true) val isOnboardingCompleted = @@ -600,6 +605,15 @@ class MainViewModel : ViewModel() { SettingsRepository.KEY_AOD_FORCE_TURN_OFF_ENABLED -> isAodForceTurnOffEnabled.value = settingsRepository.getBoolean(key) + SettingsRepository.KEY_POCKET_MODE_ENABLED -> isPocketModeEnabled.value = + settingsRepository.getBoolean(key) + + SettingsRepository.KEY_POCKET_MODE_USE_LIGHT_SENSOR -> isPocketModeUseLightSensor.value = + settingsRepository.getBoolean(key) + + SettingsRepository.KEY_POCKET_MODE_LOCK_SCREEN_ONLY -> isPocketModeLockScreenOnly.value = + settingsRepository.getBoolean(key) + SettingsRepository.KEY_NOTIFICATION_GLANCE_SAME_AS_LIGHTING -> isNotificationGlanceSameAsLightingEnabled.value = settingsRepository.getBoolean(key, true) @@ -624,6 +638,10 @@ class MainViewModel : ViewModel() { settingsRepository.getFloat(key, 0.5f) } + SettingsRepository.KEY_FLASHLIGHT_POCKET_TURN_OFF_ENABLED -> { + isFlashlightPocketTurnOffEnabled.value = settingsRepository.getBoolean(key) + } + SettingsRepository.KEY_CIRCLE_TO_SEARCH_GESTURE_ENABLED -> { isCircleToSearchGestureEnabled.value = settingsRepository.getBoolean(key) } @@ -1280,6 +1298,8 @@ class MainViewModel : ViewModel() { SettingsRepository.KEY_FLASHLIGHT_PULSE_DISABLE_ON_DND, true ) + isFlashlightPocketTurnOffEnabled.value = + settingsRepository.getBoolean(SettingsRepository.KEY_FLASHLIGHT_POCKET_TURN_OFF_ENABLED) isPitchBlackThemeEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_PITCH_BLACK_THEME_ENABLED) @@ -1439,6 +1459,14 @@ class MainViewModel : ViewModel() { settingsRepository.getBoolean(SettingsRepository.KEY_NOTIFICATION_GLANCE_ENABLED) isAodForceTurnOffEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_AOD_FORCE_TURN_OFF_ENABLED) + isPocketModeEnabled.value = + settingsRepository.getBoolean(SettingsRepository.KEY_POCKET_MODE_ENABLED) + isPocketModeUseLightSensor.value = + settingsRepository.getBoolean(SettingsRepository.KEY_POCKET_MODE_USE_LIGHT_SENSOR) + pocketModeTriggerDelay.floatValue = + settingsRepository.getFloat(SettingsRepository.KEY_POCKET_MODE_TRIGGER_DELAY, 3f) + isPocketModeLockScreenOnly.value = + settingsRepository.getBoolean(SettingsRepository.KEY_POCKET_MODE_LOCK_SCREEN_ONLY) isNotificationGlanceSameAsLightingEnabled.value = settingsRepository.getBoolean( SettingsRepository.KEY_NOTIFICATION_GLANCE_SAME_AS_LIGHTING, true @@ -3741,6 +3769,14 @@ class MainViewModel : ViewModel() { ) } + fun setFlashlightPocketTurnOffEnabled(enabled: Boolean, context: Context) { + isFlashlightPocketTurnOffEnabled.value = enabled + settingsRepository.putBoolean( + SettingsRepository.KEY_FLASHLIGHT_POCKET_TURN_OFF_ENABLED, + enabled + ) + } + fun setFlashlightFadeEnabled(enabled: Boolean, context: Context) { isFlashlightFadeEnabled.value = enabled settingsRepository.putBoolean(SettingsRepository.KEY_FLASHLIGHT_FADE_ENABLED, enabled) @@ -3922,6 +3958,26 @@ class MainViewModel : ViewModel() { isAodForceTurnOffEnabled.value = enabled } + fun setPocketModeEnabled(enabled: Boolean) { + settingsRepository.putBoolean(SettingsRepository.KEY_POCKET_MODE_ENABLED, enabled) + isPocketModeEnabled.value = enabled + } + + fun setPocketModeUseLightSensor(enabled: Boolean) { + settingsRepository.putBoolean(SettingsRepository.KEY_POCKET_MODE_USE_LIGHT_SENSOR, enabled) + isPocketModeUseLightSensor.value = enabled + } + + fun setPocketModeTriggerDelay(seconds: Float) { + pocketModeTriggerDelay.floatValue = seconds + settingsRepository.putFloat(SettingsRepository.KEY_POCKET_MODE_TRIGGER_DELAY, seconds) + } + + fun setPocketModeLockScreenOnly(enabled: Boolean) { + settingsRepository.putBoolean(SettingsRepository.KEY_POCKET_MODE_LOCK_SCREEN_ONLY, enabled) + isPocketModeLockScreenOnly.value = enabled + } + fun setNotificationGlanceSameAsLightingEnabled(enabled: Boolean) { isNotificationGlanceSameAsLightingEnabled.value = enabled settingsRepository.putBoolean( @@ -3946,6 +4002,22 @@ class MainViewModel : ViewModel() { settingsRepository.updateNotificationGlanceAppSelection(packageName, enabled) } + fun loadPocketModeExcludedApps(context: Context): List { + return settingsRepository.loadPocketModeExcludedApps() + } + + fun savePocketModeExcludedApps(context: Context, apps: List) { + settingsRepository.savePocketModeExcludedApps(apps) + } + + fun updatePocketModeExcludedAppEnabled( + context: Context, + packageName: String, + enabled: Boolean + ) { + settingsRepository.updatePocketModeExcludedAppSelection(packageName, enabled) + } + override fun onCleared() { super.onCleared() appContext?.let { context -> diff --git a/app/src/main/res/drawable/ic_light_sensor.xml b/app/src/main/res/drawable/ic_light_sensor.xml new file mode 100644 index 000000000..4b544d16f --- /dev/null +++ b/app/src/main/res/drawable/ic_light_sensor.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_pocket_mode.xml b/app/src/main/res/drawable/ic_pocket_mode.xml new file mode 100644 index 000000000..935458725 --- /dev/null +++ b/app/src/main/res/drawable/ic_pocket_mode.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a607d023f..b337e2135 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -102,6 +102,8 @@ Other Always turn off flashlight Even while display is on + Pocket detection + Turn off flashlight when phone is in your pocket Settings @@ -608,6 +610,8 @@ Vibration feedback when remapped button is pressed Flashlight toggle Toggle flashlight with volume buttons + Flashlight pocket detection + Auto-turn off flashlight when placed in pocket Enable Dynamic Night Light Master switch for dynamic night light Enable app lock @@ -987,6 +991,13 @@ pulse notification + + pocket + proximity + sensor + torch + auto + awake developer @@ -1632,4 +1643,18 @@ Music Restart Shizuku Please enter Shizuku auth token in settings first + + + Pocket mode + Prevent accidental touches while in pocket + Use light sensor + Use ambient light level in addition to proximity for better accuracy + Pocket mode active + Press any volume key to dismiss + Warning: On some devices with missing sensors, enabling Pocket Mode may break deep sleep and cause higher battery drain + Exclude apps + Choose apps where Pocket Mode should not trigger. Pocket mode will not work when streaming or gaming by default + Trigger delay + Lock screen only + Only trigger pocket mode when the device is locked