diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1399842eb..43acf0d19 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -21,6 +21,7 @@
+
diff --git a/app/src/main/java/com/sameerasw/essentials/domain/diy/Trigger.kt b/app/src/main/java/com/sameerasw/essentials/domain/diy/Trigger.kt
index f138ff9c6..25536e810 100644
--- a/app/src/main/java/com/sameerasw/essentials/domain/diy/Trigger.kt
+++ b/app/src/main/java/com/sameerasw/essentials/domain/diy/Trigger.kt
@@ -53,4 +53,58 @@ sealed interface Trigger {
override val icon: Int get() = R.drawable.rounded_nest_clock_farsight_analog_24
override val isConfigurable: Boolean get() = true
}
+
+ @Keep
+ data class BluetoothConnected(
+ @SerializedName("deviceAddress") val deviceAddress: String = "",
+ @SerializedName("deviceName") val deviceName: String = ""
+ ) : Trigger {
+ override val title: Int get() = R.string.diy_trigger_bluetooth_connected
+ override val icon: Int get() = R.drawable.rounded_bluetooth_24
+ override val isConfigurable: Boolean get() = true
+ override val permissions: List
+ get() = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
+ listOf(android.Manifest.permission.BLUETOOTH_CONNECT)
+ } else {
+ emptyList()
+ }
+ }
+
+ @Keep
+ data class BluetoothDisconnected(
+ @SerializedName("deviceAddress") val deviceAddress: String = "",
+ @SerializedName("deviceName") val deviceName: String = ""
+ ) : Trigger {
+ override val title: Int get() = R.string.diy_trigger_bluetooth_disconnected
+ override val icon: Int get() = R.drawable.rounded_bluetooth_24
+ override val isConfigurable: Boolean get() = true
+ override val permissions: List
+ get() = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
+ listOf(android.Manifest.permission.BLUETOOTH_CONNECT)
+ } else {
+ emptyList()
+ }
+ }
+
+ @Keep
+ data class WifiConnected(
+ @SerializedName("ssid") val ssid: String = ""
+ ) : Trigger {
+ override val title: Int get() = R.string.diy_trigger_wifi_connected
+ override val icon: Int get() = R.drawable.rounded_android_wifi_4_bar_plus_24
+ override val isConfigurable: Boolean get() = true
+ override val permissions: List
+ get() = listOf(android.Manifest.permission.ACCESS_FINE_LOCATION)
+ }
+
+ @Keep
+ data class WifiDisconnected(
+ @SerializedName("ssid") val ssid: String = ""
+ ) : Trigger {
+ override val title: Int get() = R.string.diy_trigger_wifi_disconnected
+ override val icon: Int get() = R.drawable.rounded_android_wifi_3_bar_24
+ override val isConfigurable: Boolean get() = true
+ override val permissions: List
+ get() = listOf(android.Manifest.permission.ACCESS_FINE_LOCATION)
+ }
}
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 1161d4869..ffd2fea11 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
@@ -1047,7 +1047,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()
},
diff --git a/app/src/main/java/com/sameerasw/essentials/services/AppDetectionService.kt b/app/src/main/java/com/sameerasw/essentials/services/AppDetectionService.kt
index 0447434dc..c9f83ef05 100644
--- a/app/src/main/java/com/sameerasw/essentials/services/AppDetectionService.kt
+++ b/app/src/main/java/com/sameerasw/essentials/services/AppDetectionService.kt
@@ -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
@@ -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,
diff --git a/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationManager.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationManager.kt
index 92d54dd00..f252dceb9 100644
--- a/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationManager.kt
+++ b/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationManager.kt
@@ -6,9 +6,11 @@ import com.sameerasw.essentials.domain.diy.Automation
import com.sameerasw.essentials.domain.diy.DIYRepository
import com.sameerasw.essentials.domain.diy.Trigger
import com.sameerasw.essentials.services.automation.modules.AutomationModule
+import com.sameerasw.essentials.services.automation.modules.BluetoothModule
import com.sameerasw.essentials.services.automation.modules.DisplayModule
import com.sameerasw.essentials.services.automation.modules.PowerModule
import com.sameerasw.essentials.services.automation.modules.TimeModule
+import com.sameerasw.essentials.services.automation.modules.WifiModule
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -63,6 +65,8 @@ object AutomationManager {
val powerAutomations = mutableListOf()
val displayAutomations = mutableListOf()
val timeAutomations = mutableListOf()
+ val bluetoothAutomations = mutableListOf()
+ val wifiAutomations = mutableListOf()
enabledAutomations.forEach { automation ->
when (automation.type) {
@@ -83,6 +87,16 @@ object AutomationManager {
timeAutomations.add(automation)
}
+ is Trigger.BluetoothConnected, is Trigger.BluetoothDisconnected -> {
+ requiredModuleIds.add(BluetoothModule.ID)
+ bluetoothAutomations.add(automation)
+ }
+
+ is Trigger.WifiConnected, is Trigger.WifiDisconnected -> {
+ requiredModuleIds.add(WifiModule.ID)
+ wifiAutomations.add(automation)
+ }
+
else -> {}
}
}
@@ -158,6 +172,26 @@ object AutomationManager {
} else {
activeModules.remove(TimeModule.ID)?.stop(context)
}
+
+ // Bluetooth Module
+ if (requiredModuleIds.contains(BluetoothModule.ID)) {
+ val module = activeModules.getOrPut(BluetoothModule.ID) {
+ BluetoothModule().also { it.start(context) }
+ }
+ module.updateAutomations(bluetoothAutomations)
+ } else {
+ activeModules.remove(BluetoothModule.ID)?.stop(context)
+ }
+
+ // Wifi Module
+ if (requiredModuleIds.contains(WifiModule.ID)) {
+ val module = activeModules.getOrPut(WifiModule.ID) {
+ WifiModule().also { it.start(context) }
+ }
+ module.updateAutomations(wifiAutomations)
+ } else {
+ activeModules.remove(WifiModule.ID)?.stop(context)
+ }
}
private fun startService(context: Context) {
diff --git a/app/src/main/java/com/sameerasw/essentials/services/automation/modules/BluetoothModule.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/modules/BluetoothModule.kt
new file mode 100644
index 000000000..7436a3779
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/services/automation/modules/BluetoothModule.kt
@@ -0,0 +1,85 @@
+package com.sameerasw.essentials.services.automation.modules
+
+import android.annotation.SuppressLint
+import android.bluetooth.BluetoothDevice
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import com.sameerasw.essentials.domain.diy.Automation
+import com.sameerasw.essentials.domain.diy.Trigger
+import com.sameerasw.essentials.services.automation.executors.CombinedActionExecutor
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class BluetoothModule : AutomationModule {
+ companion object {
+ const val ID = "bluetooth_module"
+ }
+
+ override val id: String = ID
+ private var automations: List = emptyList()
+ private val scope = CoroutineScope(Dispatchers.IO)
+
+ @SuppressLint("MissingPermission")
+ private val receiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val device = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
+ } else {
+ @Suppress("DEPRECATION")
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
+ } ?: return
+
+ val address = device.address ?: return
+
+ when (intent.action) {
+ BluetoothDevice.ACTION_ACL_CONNECTED -> handleTrigger(context, address) {
+ it is Trigger.BluetoothConnected && it.deviceAddress == address
+ }
+
+ BluetoothDevice.ACTION_ACL_DISCONNECTED -> handleTrigger(context, address) {
+ it is Trigger.BluetoothDisconnected && it.deviceAddress == address
+ }
+ }
+ }
+ }
+
+ private var appContext: Context? = null
+
+ override fun start(context: Context) {
+ val appCtx = context.applicationContext
+ appContext = appCtx
+ val filter = IntentFilter().apply {
+ addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
+ addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
+ }
+ appCtx.registerReceiver(receiver, filter)
+ }
+
+ override fun stop(context: Context) {
+ val appCtx = appContext ?: context.applicationContext
+ try {
+ appCtx.unregisterReceiver(receiver)
+ } catch (e: Exception) {
+ // Ignore if not registered
+ }
+ appContext = null
+ }
+
+ override fun updateAutomations(automations: List) {
+ this.automations = automations
+ }
+
+ private fun handleTrigger(context: Context, address: String, matches: (Trigger) -> Boolean) {
+ scope.launch {
+ automations.filter { it.type == Automation.Type.TRIGGER && it.trigger?.let(matches) == true }
+ .forEach { automation ->
+ automation.actions.forEach { action ->
+ CombinedActionExecutor.execute(context, action)
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/automation/modules/WifiModule.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/modules/WifiModule.kt
new file mode 100644
index 000000000..e846e2113
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/services/automation/modules/WifiModule.kt
@@ -0,0 +1,113 @@
+package com.sameerasw.essentials.services.automation.modules
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.wifi.WifiManager
+import android.util.Log
+import com.sameerasw.essentials.domain.diy.Automation
+import com.sameerasw.essentials.domain.diy.Trigger
+import com.sameerasw.essentials.services.automation.executors.CombinedActionExecutor
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class WifiModule : AutomationModule {
+ companion object {
+ const val ID = "wifi_module"
+ private const val UNKNOWN_SSID = ""
+ }
+
+ override val id: String = ID
+ private var automations: List = emptyList()
+ private val scope = CoroutineScope(Dispatchers.IO)
+ private var connectivityManager: ConnectivityManager? = null
+ private var appContext: Context? = null
+ private val activeNetworkSsids = java.util.concurrent.ConcurrentHashMap()
+
+ private val networkCallback = object : ConnectivityManager.NetworkCallback() {
+ override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) {
+ if (!capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) return
+ val ssid = currentWifiSsid()
+ if (ssid == null) {
+ Log.d(ID, "Wi-Fi capabilities changed but SSID unavailable (check location permission/services)")
+ return
+ }
+ if (activeNetworkSsids[network] == ssid) return
+ Log.d(ID, "Wi-Fi connected: $ssid")
+ activeNetworkSsids[network] = ssid
+ handleTrigger { it is Trigger.WifiConnected && it.ssid == ssid }
+ }
+
+ override fun onLost(network: Network) {
+ val ssid = activeNetworkSsids.remove(network) ?: return
+ Log.d(ID, "Wi-Fi disconnected: $ssid")
+ handleTrigger { it is Trigger.WifiDisconnected && it.ssid == ssid }
+ }
+ }
+
+ override fun start(context: Context) {
+ appContext = context.applicationContext
+ val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ connectivityManager = manager
+ val request = NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ // Default builder requires validated internet access; many Wi-Fi networks
+ // (captive portals, offline IoT/local networks) never satisfy that, so the
+ // callback would otherwise never fire for them.
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build()
+ try {
+ manager.registerNetworkCallback(request, networkCallback)
+ } catch (e: Exception) {
+ Log.e(ID, "Failed to register Wi-Fi network callback", e)
+ }
+ }
+
+ // NetworkCapabilities.getTransportInfo() only exists from API 31 onward, and querying it
+ // on older OS versions throws NoSuchMethodError inside the (system-swallowed) callback.
+ // WifiManager.connectionInfo works on every API level and respects the same location
+ // permission gating, so it's used unconditionally instead of branching by SDK level.
+ @Suppress("DEPRECATION")
+ private fun currentWifiSsid(): String? {
+ val wifiManager =
+ appContext?.getSystemService(Context.WIFI_SERVICE) as? WifiManager ?: return null
+ val ssid = try {
+ wifiManager.connectionInfo?.ssid
+ } catch (e: Exception) {
+ Log.w(ID, "Failed to read current SSID", e)
+ null
+ }
+ if (ssid.isNullOrEmpty() || ssid == UNKNOWN_SSID) return null
+ return ssid.removeSurrounding("\"")
+ }
+
+ override fun stop(context: Context) {
+ try {
+ connectivityManager?.unregisterNetworkCallback(networkCallback)
+ } catch (e: Exception) {
+ // Ignore if not registered
+ }
+ connectivityManager = null
+ appContext = null
+ activeNetworkSsids.clear()
+ }
+
+ override fun updateAutomations(automations: List) {
+ this.automations = automations
+ }
+
+ private fun handleTrigger(matches: (Trigger) -> Boolean) {
+ val context = appContext ?: return
+ scope.launch {
+ automations.filter { it.type == Automation.Type.TRIGGER && it.trigger?.let(matches) == true }
+ .forEach { automation ->
+ automation.actions.forEach { action ->
+ CombinedActionExecutor.execute(context, action)
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt
index 1a6b75f94..cab12289f 100644
--- a/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt
+++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt
@@ -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
@@ -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) {
@@ -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
}
@@ -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 =
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 26d8eaf1d..a558cbfc1 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
@@ -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)
}
}
@@ -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
}
}
@@ -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)
@@ -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,
@@ -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,
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 264b570de..cdc25d8d4 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
@@ -70,9 +70,11 @@ import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer
import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenu
import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem
import com.sameerasw.essentials.ui.components.pickers.SegmentedPicker
+import com.sameerasw.essentials.ui.components.sheets.BluetoothDeviceSelectionSheet
import com.sameerasw.essentials.ui.components.sheets.DimWallpaperSettingsSheet
import com.sameerasw.essentials.ui.components.sheets.ScreenOffSettingsSheet
import com.sameerasw.essentials.ui.components.sheets.SoundModeSettingsSheet
+import com.sameerasw.essentials.ui.components.sheets.WifiNetworkSelectionSheet
import com.sameerasw.essentials.ui.theme.EssentialsTheme
import com.sameerasw.essentials.utils.AppUtil
import com.sameerasw.essentials.utils.HapticUtil
@@ -228,10 +230,20 @@ class AutomationEditorActivity : ComponentActivity() {
var showDeviceEffectsSettings by remember { mutableStateOf(false) }
var showSoundModeSettings by remember { mutableStateOf(false) }
var showTimeSettings by remember { mutableStateOf(false) }
+ var showBluetoothSettings by remember { mutableStateOf(false) }
+ var showWifiSettings by remember { mutableStateOf(false) }
var configAction by remember { mutableStateOf(null) } // Generic config action
+ val isTriggerConfigured = when (val trigger = selectedTrigger) {
+ is Trigger.BluetoothConnected -> trigger.deviceAddress.isNotBlank()
+ is Trigger.BluetoothDisconnected -> trigger.deviceAddress.isNotBlank()
+ is Trigger.WifiConnected -> trigger.ssid.isNotBlank()
+ is Trigger.WifiDisconnected -> trigger.ssid.isNotBlank()
+ else -> true
+ }
+
val isValid = when (automationType) {
- Automation.Type.TRIGGER -> selectedTrigger != null && selectedAction != null
+ Automation.Type.TRIGGER -> selectedTrigger != null && selectedAction != null && isTriggerConfigured
Automation.Type.ACTION_SHORTCUT, Automation.Type.PIXEL_SEARCHBAR -> selectedAction != null
Automation.Type.STATE -> selectedState != null && (selectedInAction != null || selectedOutAction != null)
Automation.Type.APP -> selectedApps.isNotEmpty() && (selectedInAction != null || selectedOutAction != null)
@@ -504,6 +516,26 @@ class AutomationEditorActivity : ComponentActivity() {
?: 0,
days = (selectedTrigger as? Trigger.Schedule)?.days
?: emptySet()
+ ),
+ Trigger.BluetoothConnected(
+ deviceAddress = (selectedTrigger as? Trigger.BluetoothConnected)?.deviceAddress
+ ?: "",
+ deviceName = (selectedTrigger as? Trigger.BluetoothConnected)?.deviceName
+ ?: ""
+ ),
+ Trigger.BluetoothDisconnected(
+ deviceAddress = (selectedTrigger as? Trigger.BluetoothDisconnected)?.deviceAddress
+ ?: "",
+ deviceName = (selectedTrigger as? Trigger.BluetoothDisconnected)?.deviceName
+ ?: ""
+ ),
+ Trigger.WifiConnected(
+ ssid = (selectedTrigger as? Trigger.WifiConnected)?.ssid
+ ?: ""
+ ),
+ Trigger.WifiDisconnected(
+ ssid = (selectedTrigger as? Trigger.WifiDisconnected)?.ssid
+ ?: ""
)
)
triggers.forEach { trigger ->
@@ -514,8 +546,19 @@ class AutomationEditorActivity : ComponentActivity() {
isConfigurable = trigger.isConfigurable,
onClick = { selectedTrigger = trigger },
onSettingsClick = {
- if (trigger is Trigger.Schedule) {
- showTimeSettings = true
+ when (trigger) {
+ is Trigger.Schedule -> showTimeSettings =
+ true
+
+ is Trigger.BluetoothConnected,
+ is Trigger.BluetoothDisconnected -> showBluetoothSettings =
+ true
+
+ is Trigger.WifiConnected,
+ is Trigger.WifiDisconnected -> showWifiSettings =
+ true
+
+ else -> {}
}
}
)
@@ -720,6 +763,47 @@ class AutomationEditorActivity : ComponentActivity() {
)
}
+ if (showBluetoothSettings) {
+ BluetoothDeviceSelectionSheet(
+ onDismiss = { showBluetoothSettings = false },
+ onSave = { address, name ->
+ selectedTrigger = when (selectedTrigger) {
+ is Trigger.BluetoothConnected -> Trigger.BluetoothConnected(
+ deviceAddress = address,
+ deviceName = name
+ )
+
+ is Trigger.BluetoothDisconnected -> Trigger.BluetoothDisconnected(
+ deviceAddress = address,
+ deviceName = name
+ )
+
+ else -> selectedTrigger
+ }
+ showBluetoothSettings = false
+ }
+ )
+ }
+
+ if (showWifiSettings) {
+ WifiNetworkSelectionSheet(
+ initialSsid = when (val trigger = selectedTrigger) {
+ is Trigger.WifiConnected -> trigger.ssid
+ is Trigger.WifiDisconnected -> trigger.ssid
+ else -> null
+ },
+ onDismiss = { showWifiSettings = false },
+ onSave = { ssid ->
+ selectedTrigger = when (selectedTrigger) {
+ is Trigger.WifiConnected -> Trigger.WifiConnected(ssid = ssid)
+ is Trigger.WifiDisconnected -> Trigger.WifiDisconnected(ssid = ssid)
+ else -> selectedTrigger
+ }
+ showWifiSettings = false
+ }
+ )
+ }
+
if (showDimSettings && configAction is Action.DimWallpaper) {
DimWallpaperSettingsSheet(
initialAction = configAction as Action.DimWallpaper,
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/SelectionCardItem.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/SelectionCardItem.kt
new file mode 100644
index 000000000..30b988188
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/SelectionCardItem.kt
@@ -0,0 +1,55 @@
+package com.sameerasw.essentials.ui.components.cards
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.ListItemDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.sameerasw.essentials.utils.HapticUtil
+
+@Composable
+fun SelectionCardItem(
+ title: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ description: String? = null,
+ iconRes: Int? = null
+) {
+ val view = LocalView.current
+
+ ListItem(
+ modifier = modifier
+ .fillMaxWidth()
+ .clip(MaterialTheme.shapes.extraSmall)
+ .clickable {
+ HapticUtil.performUIHaptic(view)
+ onClick()
+ },
+ headlineContent = { Text(title) },
+ supportingContent = if (description != null) {
+ { Text(description) }
+ } else null,
+ leadingContent = if (iconRes != null && iconRes != 0) {
+ {
+ Icon(
+ painter = painterResource(iconRes),
+ contentDescription = null,
+ modifier = Modifier.size(24.dp),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ }
+ } else null,
+ colors = ListItemDefaults.colors(
+ containerColor = MaterialTheme.colorScheme.surfaceBright
+ )
+ )
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/BluetoothDeviceSelectionSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/BluetoothDeviceSelectionSheet.kt
new file mode 100644
index 000000000..90bc6dcd7
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/BluetoothDeviceSelectionSheet.kt
@@ -0,0 +1,115 @@
+package com.sameerasw.essentials.ui.components.sheets
+
+import android.Manifest
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.core.content.ContextCompat
+import com.sameerasw.essentials.R
+import com.sameerasw.essentials.ui.components.cards.SelectionCardItem
+import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer
+import com.sameerasw.essentials.utils.BluetoothPairedDevicesUtil
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun BluetoothDeviceSelectionSheet(
+ onDismiss: () -> Unit,
+ onSave: (address: String, name: String) -> Unit
+) {
+ val context = LocalContext.current
+
+ var hasPermission by remember {
+ mutableStateOf(
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.S ||
+ ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.BLUETOOTH_CONNECT
+ ) == PackageManager.PERMISSION_GRANTED
+ )
+ }
+
+ val permissionLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestPermission()
+ ) { granted -> hasPermission = granted }
+
+ LaunchedEffect(Unit) {
+ if (!hasPermission) {
+ permissionLauncher.launch(Manifest.permission.BLUETOOTH_CONNECT)
+ }
+ }
+
+ val devices = remember(hasPermission) {
+ if (hasPermission) BluetoothPairedDevicesUtil.getPairedDevices(context) else emptyList()
+ }
+
+ ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
+ dragHandle = null
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(24.dp)
+ .fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.diy_select_bluetooth_device_title),
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+
+ if (!hasPermission) {
+ Text(
+ text = stringResource(R.string.diy_bluetooth_permission_required),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ } else if (devices.isEmpty()) {
+ Text(
+ text = stringResource(R.string.diy_no_paired_bluetooth_devices),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ } else {
+ RoundedCardContainer {
+ LazyColumn(
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ items(devices) { device ->
+ SelectionCardItem(
+ title = device.name,
+ description = device.address,
+ iconRes = R.drawable.rounded_bluetooth_24,
+ onClick = { onSave(device.address, device.name) }
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/WifiNetworkSelectionSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/WifiNetworkSelectionSheet.kt
new file mode 100644
index 000000000..230922a56
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/WifiNetworkSelectionSheet.kt
@@ -0,0 +1,219 @@
+package com.sameerasw.essentials.ui.components.sheets
+
+import android.Manifest
+import android.content.pm.PackageManager
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.ListItemDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.core.content.ContextCompat
+import com.sameerasw.essentials.R
+import com.sameerasw.essentials.ui.components.cards.SelectionCardItem
+import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer
+import com.sameerasw.essentials.utils.HapticUtil
+import com.sameerasw.essentials.utils.WifiUtil
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun WifiNetworkSelectionSheet(
+ initialSsid: String? = null,
+ onDismiss: () -> Unit,
+ onSave: (ssid: String) -> Unit
+) {
+ val context = LocalContext.current
+ val view = LocalView.current
+
+ var ssid by remember { mutableStateOf(initialSsid ?: "") }
+ var isLoadingSavedNetworks by remember { mutableStateOf(true) }
+ var savedNetworks by remember { mutableStateOf>(emptyList()) }
+
+ LaunchedEffect(Unit) {
+ val networks = withContext(Dispatchers.IO) {
+ if (WifiUtil.canReadSavedNetworks(context)) {
+ WifiUtil.getSavedNetworkSsids(context)
+ } else {
+ emptyList()
+ }
+ }
+ savedNetworks = networks
+ isLoadingSavedNetworks = false
+ }
+
+ val locationPermissionLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestPermission()
+ ) { granted ->
+ if (granted) {
+ WifiUtil.getCurrentSsid(context)?.let { ssid = it }
+ }
+ }
+
+ ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
+ dragHandle = null
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(24.dp)
+ .fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.diy_select_wifi_network_title),
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+
+ when {
+ isLoadingSavedNetworks -> {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 24.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator()
+ }
+ }
+
+ savedNetworks.isNotEmpty() -> {
+ Text(
+ text = stringResource(R.string.diy_saved_networks_via_shizuku),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ RoundedCardContainer {
+ LazyColumn(
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ items(savedNetworks) { network ->
+ SelectionCardItem(
+ title = network,
+ iconRes = R.drawable.rounded_android_wifi_4_bar_plus_24,
+ onClick = { onSave(network) }
+ )
+ }
+ }
+ }
+ }
+
+ else -> {
+ OutlinedTextField(
+ value = ssid,
+ onValueChange = { ssid = it },
+ label = { Text(stringResource(R.string.diy_wifi_ssid_label)) },
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true
+ )
+
+ OutlinedButton(
+ onClick = {
+ HapticUtil.performUIHaptic(view)
+ val hasPermission = ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED
+ if (hasPermission) {
+ WifiUtil.getCurrentSsid(context)?.let { ssid = it }
+ } else {
+ locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
+ }
+ },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.rounded_android_wifi_4_bar_plus_24),
+ contentDescription = null,
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(modifier = Modifier.size(8.dp))
+ Text(stringResource(R.string.diy_use_current_network))
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Button(
+ onClick = {
+ HapticUtil.performVirtualKeyHaptic(view)
+ onDismiss()
+ },
+ modifier = Modifier.weight(1f),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.surfaceBright,
+ contentColor = MaterialTheme.colorScheme.onSurface
+ )
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.rounded_close_24),
+ contentDescription = null,
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(modifier = Modifier.size(8.dp))
+ Text(stringResource(R.string.action_cancel))
+ }
+
+ Button(
+ onClick = {
+ HapticUtil.performVirtualKeyHaptic(view)
+ if (ssid.isNotBlank()) {
+ onSave(ssid.trim())
+ }
+ },
+ modifier = Modifier.weight(1f),
+ enabled = ssid.isNotBlank()
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.rounded_check_24),
+ contentDescription = null,
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(modifier = Modifier.size(8.dp))
+ Text(stringResource(R.string.action_save))
+ }
+ }
+ }
+ }
+ }
+ }
+}
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
index 0e82ae603..eb2754d20 100644
--- 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
@@ -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,
diff --git a/app/src/main/java/com/sameerasw/essentials/utils/BluetoothPairedDevicesUtil.kt b/app/src/main/java/com/sameerasw/essentials/utils/BluetoothPairedDevicesUtil.kt
new file mode 100644
index 000000000..24ed00c52
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/utils/BluetoothPairedDevicesUtil.kt
@@ -0,0 +1,35 @@
+package com.sameerasw.essentials.utils
+
+import android.annotation.SuppressLint
+import android.bluetooth.BluetoothManager
+import android.content.Context
+import androidx.annotation.Keep
+
+object BluetoothPairedDevicesUtil {
+
+ @Keep
+ data class PairedDevice(
+ val name: String,
+ val address: String
+ )
+
+ @SuppressLint("MissingPermission")
+ fun getPairedDevices(context: Context): List {
+ val bluetoothManager =
+ context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
+ val adapter = bluetoothManager?.adapter ?: return emptyList()
+
+ val devices = try {
+ adapter.bondedDevices
+ } catch (e: SecurityException) {
+ return emptyList()
+ }
+
+ return devices.map { device ->
+ PairedDevice(
+ name = device.alias ?: device.name ?: device.address,
+ address = device.address
+ )
+ }.sortedBy { it.name.lowercase() }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/utils/WifiUtil.kt b/app/src/main/java/com/sameerasw/essentials/utils/WifiUtil.kt
new file mode 100644
index 000000000..79c98602c
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/utils/WifiUtil.kt
@@ -0,0 +1,38 @@
+package com.sameerasw.essentials.utils
+
+import android.content.Context
+import android.net.wifi.WifiManager
+
+object WifiUtil {
+ private const val UNKNOWN_SSID = ""
+ private val SSID_REGEX = Regex("\"([^\"]*)\"")
+
+ @Suppress("DEPRECATION")
+ fun getCurrentSsid(context: Context): String? {
+ val wifiManager =
+ context.applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager
+ ?: return null
+ val ssid = wifiManager.connectionInfo?.ssid ?: return null
+ if (ssid.isEmpty() || ssid == UNKNOWN_SSID) return null
+ return ssid.removeSurrounding("\"")
+ }
+
+ /**
+ * Shizuku/root grant shell-level privileges, which can run `cmd wifi list-networks`
+ * (normally restricted to system apps) to read the device's saved Wi-Fi networks.
+ */
+ fun canReadSavedNetworks(context: Context): Boolean {
+ return ShellUtils.isAvailable(context) && ShellUtils.hasPermission(context)
+ }
+
+ fun getSavedNetworkSsids(context: Context): List {
+ val output = ShellUtils.runCommandWithOutput(context, "cmd wifi list-networks")
+ ?: return emptyList()
+ return output.lineSequence()
+ .drop(1) // header row: "Network Id SSID Security type"
+ .mapNotNull { line -> SSID_REGEX.find(line)?.groupValues?.get(1) }
+ .filter { it.isNotBlank() }
+ .distinct()
+ .toList()
+ }
+}
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 8ad672035..33b2d896d 100644
--- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
+++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
@@ -4142,9 +4142,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) {
@@ -4192,6 +4193,7 @@ class MainViewModel : ViewModel() {
fun savePocketModeExcludedApps(context: Context, apps: List) {
settingsRepository.savePocketModeExcludedApps(apps)
+ updateAppDetectionService(context)
}
fun updatePocketModeExcludedAppEnabled(
@@ -4200,6 +4202,7 @@ class MainViewModel : ViewModel() {
enabled: Boolean
) {
settingsRepository.updatePocketModeExcludedAppSelection(packageName, enabled)
+ updateAppDetectionService(context)
}
override fun onCleared() {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 57389c37f..959dc4ed6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -877,6 +877,17 @@
Charger Connected
Charger Disconnected
Schedule
+ Bluetooth Device Connected
+ Bluetooth Device Disconnected
+ Wi-Fi Network Connected
+ Wi-Fi Network Disconnected
+ Select Bluetooth Device
+ Bluetooth permission is required to list paired devices.
+ No paired Bluetooth devices found. Pair a device first in system settings.
+ Select Wi-Fi Network
+ Network name (SSID)
+ Use Current Network
+ Saved networks (via Shizuku)
Time Period
Select Time
Select Time Range