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