Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,7 @@ object FeatureRegistry {
override fun isEnabled(viewModel: MainViewModel) = viewModel.isPocketModeEnabled.value

override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) =
viewModel.setPocketModeEnabled(enabled)
viewModel.setPocketModeEnabled(enabled, context)

override fun isDeviceSupported(context: Context) = !DeviceUtils.isGoogleDevice()
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.app.usage.UsageEvents
import android.app.usage.UsageStats
import android.app.usage.UsageStatsManager
import android.content.BroadcastReceiver
Expand Down Expand Up @@ -101,6 +102,26 @@ class AppDetectionService : Service() {
private fun getForegroundPackage(): String? {
val usageStatsManager = getSystemService(USAGE_STATS_SERVICE) as UsageStatsManager
val time = System.currentTimeMillis()

// 1. Try to find the last resumed activity using queryEvents (real-time & accurate)
try {
val events = usageStatsManager.queryEvents(time - 1000 * 15, time)
val event = UsageEvents.Event()
var lastResumedPackage: String? = null
while (events.hasNextEvent()) {
events.getNextEvent(event)
if (event.eventType == UsageEvents.Event.ACTIVITY_RESUMED) {
lastResumedPackage = event.packageName
}
}
if (lastResumedPackage != null) {
return lastResumedPackage
}
} catch (e: Exception) {
android.util.Log.e("AppDetectionService", "Failed to query usage events", e)
}

// 2. Fallback to queryUsageStats if no events found in the window
val stats = usageStatsManager.queryUsageStats(
UsageStatsManager.INTERVAL_DAILY,
time - 1000 * 10,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.media.AudioManager
import android.os.Handler
import android.os.Looper
import android.view.inputmethod.InputMethodManager
import android.provider.Settings
import android.util.Log
import androidx.core.app.NotificationCompat
Expand Down Expand Up @@ -105,14 +107,67 @@ class AppFlowHandler(
private val ignoredSystemPackages = listOf(
"android",
"com.android.systemui",
"com.google.android.inputmethod.latin"
"com.google.android.inputmethod.latin",
"com.google.android.gms"
)

private fun isIgnoredPackage(packageName: String): Boolean {
if (ignoredSystemPackages.contains(packageName)) return true

val lowerPkg = packageName.lowercase()
if (lowerPkg.contains("systemui") ||
lowerPkg.contains("keyguard") ||
lowerPkg.contains("volume") ||
lowerPkg.contains("soundassistant") ||
lowerPkg.contains("dialer") ||
lowerPkg.contains("telecom") ||
lowerPkg.contains("phone") ||
lowerPkg.contains("incallui") ||
lowerPkg.contains("packageinstaller") ||
lowerPkg.contains("permissioncontroller")
) {
return true
}

// Check active call state via AudioManager mode
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
if (audioManager != null) {
val mode = audioManager.mode
if (mode == AudioManager.MODE_IN_CALL ||
mode == AudioManager.MODE_IN_COMMUNICATION ||
mode == AudioManager.MODE_RINGTONE
) {
return true
}
}

return try {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
val ims = imm?.enabledInputMethodList
ims?.any { it.packageName == packageName } == true
} catch (_: Exception) {
false
}
}

fun onPackageChanged(packageName: String, isFromUsageStats: Boolean = false) {
val prefs = context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE)
val useUsageAccess = prefs.getBoolean("use_usage_access", false)

Log.d("AppFlowHandler", "onPackageChanged: packageName=$packageName, isFromUsageStats=$isFromUsageStats, useUsageAccess=$useUsageAccess, currentPackage=$currentPackage")

if (isIgnoredPackage(packageName)) {
Log.d("AppFlowHandler", "onPackageChanged: Ignoring system/IME/volume/call package $packageName")
return
}

val oldPackage = currentPackage

val serviceInstance = com.sameerasw.essentials.services.tiles.ScreenOffAccessibilityService.instance
if (serviceInstance != null && serviceInstance.isAppBypassedForPocketMode(packageName)) {
serviceInstance.dismissPocketMode()
}

if (isFromUsageStats == useUsageAccess) {
currentPackage = packageName
if (oldPackage != null && oldPackage != packageName) {
Expand Down Expand Up @@ -215,7 +270,7 @@ class AppFlowHandler(

pendingNLRunnable?.let { handler.removeCallbacks(it) }

if (ignoredSystemPackages.contains(packageName)) {
if (isIgnoredPackage(packageName)) {
Log.d("NightLight", "Ignoring system package $packageName")
return
}
Expand Down Expand Up @@ -287,6 +342,10 @@ class AppFlowHandler(
}

private fun checkAppAutomations(packageName: String) {
if (isIgnoredPackage(packageName)) {
Log.d("AppFlowHandler", "checkAppAutomations: Ignoring system/IME package $packageName")
return
}
scope.launch {
val automations = DIYRepository.automations.value
val appAutomations =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,10 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene
statusBarIconHandler.updateAll()
} else if (key == "pocket_mode_enabled" || key == "pocket_mode_use_light_sensor") {
updatePocketModeSensors()
com.sameerasw.essentials.utils.ServiceUtils.startRequiredServices(this)
} else if (key == "pocket_mode_excluded_apps") {
updatePocketModeExcludedAppsSet()
com.sameerasw.essentials.utils.ServiceUtils.startRequiredServices(this)
}
}

Expand Down Expand Up @@ -249,7 +251,9 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene

Intent.ACTION_USER_PRESENT -> {
val prefs = getSharedPreferences("essentials_prefs", MODE_PRIVATE)
if (prefs.getBoolean("pocket_mode_lock_screen_only", false)) {
val lockScreenOnly = prefs.getBoolean("pocket_mode_lock_screen_only", false)
val currentApp = appFlowHandler.currentPackage
if (lockScreenOnly || isAppBypassedForPocketMode(currentApp)) {
pocketModeHandler.onScreenOff() // cancel pending timer + remove overlay
}
}
Expand Down Expand Up @@ -389,6 +393,66 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene

override fun onInterrupt() {}

fun isAppBypassedForPocketMode(packageName: String?): Boolean {
val prefs = getSharedPreferences("essentials_prefs", MODE_PRIVATE)
val lockScreenOnly = prefs.getBoolean("pocket_mode_lock_screen_only", false)
if (lockScreenOnly && !keyguardManager.isKeyguardLocked) {
return true
}

val checkedPackage = if (packageName != null && (
pocketModeExcludedAppsSet.contains(packageName) ||
isGameOrVideoApp(packageName) ||
hasActiveMediaSession(packageName)
)) {
packageName
} else {
val realPkg = getActivePackageName()
if (realPkg != null && (
pocketModeExcludedAppsSet.contains(realPkg) ||
isGameOrVideoApp(realPkg) ||
hasActiveMediaSession(realPkg)
)) {
realPkg
} else {
packageName
}
}

val isExcluded = !keyguardManager.isKeyguardLocked && checkedPackage != null && (
pocketModeExcludedAppsSet.contains(checkedPackage) ||
isGameOrVideoApp(checkedPackage) ||
hasActiveMediaSession(checkedPackage)
)
return isExcluded
}

private fun getActivePackageName(): String? {
val prefs = getSharedPreferences("essentials_prefs", MODE_PRIVATE)
val useUsageAccess = prefs.getBoolean("use_usage_access", false)
if (useUsageAccess) {
try {
val usm = getSystemService(USAGE_STATS_SERVICE) as? android.app.usage.UsageStatsManager
val time = System.currentTimeMillis()
val events = usm?.queryEvents(time - 1000 * 15, time)
val event = android.app.usage.UsageEvents.Event()
var lastResumed: String? = null
while (events?.hasNextEvent() == true) {
events.getNextEvent(event)
if (event.eventType == android.app.usage.UsageEvents.Event.ACTIVITY_RESUMED) {
lastResumed = event.packageName
}
}
if (lastResumed != null) return lastResumed
} catch (_: Exception) {}
}
return rootInActiveWindow?.packageName?.toString()
}

fun dismissPocketMode() {
pocketModeHandler.onScreenOff()
}

private fun updatePocketModeSensors() {
val prefs = getSharedPreferences("essentials_prefs", MODE_PRIVATE)
val pocketModeEnabled = prefs.getBoolean("pocket_mode_enabled", false)
Expand Down Expand Up @@ -434,15 +498,12 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene
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) {
val shouldBypass = isAppBypassedForPocketMode(currentApp)
if (shouldBypass) {
pocketModeHandler.onScreenOff()
} else {
pocketModeHandler.onProximityChanged(
isBlocked = flashlightHandler.isProximityBlocked,
isLightDark = lightSensorLux <= 3f,
Expand Down Expand Up @@ -470,15 +531,12 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene
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) {
val shouldBypass = isAppBypassedForPocketMode(currentApp)
if (shouldBypass) {
pocketModeHandler.onScreenOff()
} else {
pocketModeHandler.onProximityChanged(
isBlocked = isBlocked,
isLightDark = lightSensorLux <= 3f,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ fun PocketModeSettingsUI(
description = stringResource(R.string.feat_pocket_mode_desc),
isChecked = viewModel.isPocketModeEnabled.value,
onCheckedChange = { isChecked ->
viewModel.setPocketModeEnabled(isChecked)
viewModel.setPocketModeEnabled(isChecked, context)
},
enabled = true,
iconRes = R.drawable.ic_pocket_mode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4035,9 +4035,10 @@ class MainViewModel : ViewModel() {
isAodForceTurnOffEnabled.value = enabled
}

fun setPocketModeEnabled(enabled: Boolean) {
fun setPocketModeEnabled(enabled: Boolean, context: Context) {
settingsRepository.putBoolean(SettingsRepository.KEY_POCKET_MODE_ENABLED, enabled)
isPocketModeEnabled.value = enabled
updateAppDetectionService(context)
}

fun setPocketModeUseLightSensor(enabled: Boolean) {
Expand Down Expand Up @@ -4085,6 +4086,7 @@ class MainViewModel : ViewModel() {

fun savePocketModeExcludedApps(context: Context, apps: List<AppSelection>) {
settingsRepository.savePocketModeExcludedApps(apps)
updateAppDetectionService(context)
}

fun updatePocketModeExcludedAppEnabled(
Expand All @@ -4093,6 +4095,7 @@ class MainViewModel : ViewModel() {
enabled: Boolean
) {
settingsRepository.updatePocketModeExcludedAppSelection(packageName, enabled)
updateAppDetectionService(context)
}

override fun onCleared() {
Expand Down