Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,19 @@
android:value="android.service.quicksettings.CATEGORY_DISPLAY" />
</service>

<service
android:name=".services.tiles.PocketModeTileService"
android:exported="true"
android:icon="@drawable/ic_pocket_mode"
android:label="@string/feat_pocket_mode_title"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
<meta-data android:name="android.service.quicksettings.TILE_CATEGORY"
android:value="android.service.quicksettings.CATEGORY_DISPLAY" />
</service>

<receiver
android:name=".services.receivers.SecurityDeviceAdminReceiver"
android:permission="android.permission.BIND_DEVICE_ADMIN"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import com.sameerasw.essentials.ui.composables.configs.OtherCustomizationsSettin
import com.sameerasw.essentials.ui.composables.configs.QuickSettingsTilesSettingsUI
import com.sameerasw.essentials.ui.composables.configs.RefreshRateSettingsUI
import com.sameerasw.essentials.ui.composables.configs.RemoteLockSettingsUI
import com.sameerasw.essentials.ui.composables.configs.PocketModeSettingsUI
import com.sameerasw.essentials.ui.composables.configs.ScreenLockedSecuritySettingsUI
import com.sameerasw.essentials.ui.composables.configs.ScreenOffWidgetSettingsUI
import com.sameerasw.essentials.ui.composables.configs.ShutUpSettingsUI
Expand Down Expand Up @@ -285,6 +286,7 @@ class FeatureSettingsActivity : AppCompatActivity() {
"Statusbar icons" -> !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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<AppSelection>) =
saveAppSelection(KEY_POCKET_MODE_EXCLUDED_APPS, apps)
fun updatePocketModeExcludedAppSelection(packageName: String, enabled: Boolean) =
updateAppSelection(KEY_POCKET_MODE_EXCLUDED_APPS, packageName, enabled)

fun loadShutUpConfigs(): List<com.sameerasw.essentials.domain.model.ShutUpAppConfig> {
val json = prefs.getString(KEY_SHUT_UP_SELECTED_APPS, null)
return if (json != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
}
}
}
}
Loading