From b464185c6cdd652932897b2bd0c2d327bfdc98b9 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Fri, 21 Aug 2026 15:20:29 +0530 Subject: [PATCH 1/7] chore: add backup file pattern to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3f88340a4..604338b51 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ build/ local.properties .agents/ +*build.gradle.kts.bak From 792df852672a71671852d6736bd77c0af9166cf2 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Fri, 21 Aug 2026 15:42:31 +0530 Subject: [PATCH 2/7] refactor: migrate button remap actions to DIY actions for consistency --- .../data/repository/SettingsRepository.kt | 56 +++ .../sameerasw/essentials/domain/diy/Action.kt | 33 +- .../domain/diy/ActionGsonAdapter.kt | 65 +++ .../essentials/domain/diy/ActionRegistry.kt | 101 ++++ .../services/InputEventListenerService.kt | 29 +- .../executors/CombinedActionExecutor.kt | 42 +- .../services/handlers/ButtonRemapHandler.kt | 183 ++----- .../ui/activities/AutomationEditorActivity.kt | 63 +-- .../hardware/ButtonRemapSettingsUI.kt | 453 +++++++++++++----- .../essentials/viewmodels/DIYViewModel.kt | 2 - .../essentials/viewmodels/MainViewModel.kt | 61 +-- app/src/main/res/values/strings.xml | 3 + 12 files changed, 682 insertions(+), 409 deletions(-) create mode 100644 app/src/main/java/com/sameerasw/essentials/domain/diy/ActionGsonAdapter.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/domain/diy/ActionRegistry.kt 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 d2c5244e2..858d6686b 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 @@ -14,6 +14,8 @@ import android.content.SharedPreferences import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.sameerasw.essentials.domain.HapticFeedbackType +import com.sameerasw.essentials.domain.diy.Action +import com.sameerasw.essentials.domain.diy.ActionGsonAdapter import com.sameerasw.essentials.domain.model.AppSelection import com.sameerasw.essentials.domain.model.AppTag import com.sameerasw.essentials.domain.model.DnsPreset @@ -38,6 +40,7 @@ class SettingsRepository(private val context: Context) { init { migrateUsageAccessKey() + migrateRemapStringToAction() } private fun migrateUsageAccessKey() { @@ -51,6 +54,58 @@ class SettingsRepository(private val context: Context) { } } + private fun migrateRemapStringToAction() { + if (getBoolean(KEY_BUTTON_REMAP_MIGRATION_DONE)) return + + val remapKeys = listOf( + KEY_BUTTON_REMAP_VOL_UP_ACTION_OFF, + KEY_BUTTON_REMAP_VOL_DOWN_ACTION_OFF, + KEY_BUTTON_REMAP_VOL_UP_ACTION_ON, + KEY_BUTTON_REMAP_VOL_DOWN_ACTION_ON + ) + for (key in remapKeys) { + val raw = prefs.getString(key, null) ?: continue + // Skip if already JSON (starts with '{') — already migrated or set by new code + if (raw.startsWith("{")) continue + val action: Action? = when (raw) { + "Toggle flashlight" -> Action.ToggleFlashlight + "Media play/pause" -> Action.MediaPlayPause + "Media next" -> Action.MediaNext + "Media previous" -> Action.MediaPrevious + "Toggle vibrate" -> Action.ToggleVibrate + "Toggle mute" -> Action.ToggleMute + "AI assistant" -> Action.AIAssistant + "Take screenshot" -> Action.TakeScreenshot + "Cycle sound modes" -> Action.CycleSoundModes + "Toggle media volume" -> Action.ToggleMediaVolume + "Like current song" -> Action.LikeCurrentSong + "Circle to Search" -> Action.CircleToSearch + else -> null + } + setRemapAction(key, action) + } + + putBoolean(KEY_BUTTON_REMAP_MIGRATION_DONE, true) + } + + fun getRemapAction(key: String): Action? { + val json = prefs.getString(key, null) ?: return null + return try { + ActionGsonAdapter.fromJson(json) + } catch (_: Exception) { + null + } + } + + fun setRemapAction(key: String, action: Action?) { + if (action == null) { + prefs.edit().remove(key).apply() + } else { + prefs.edit().putString(key, ActionGsonAdapter.toJson(action)).apply() + } + } + + companion object { const val PREFS_NAME = "essentials_prefs" @@ -122,6 +177,7 @@ class SettingsRepository(private val context: Context) { const val KEY_BUTTON_REMAP_VOL_DOWN_ACTION_ON = "button_remap_vol_down_action_on" const val KEY_BUTTON_REMAP_HAPTIC_TYPE = "button_remap_haptic_type" const val KEY_FLASHLIGHT_HAPTIC_TYPE = "flashlight_haptic_type" // Legacy + const val KEY_BUTTON_REMAP_MIGRATION_DONE = "button_remap_action_migration_done" const val KEY_DYNAMIC_NIGHT_LIGHT_ENABLED = "dynamic_night_light_enabled" const val KEY_DYNAMIC_NIGHT_LIGHT_SELECTED_APPS = "dynamic_night_light_selected_apps" diff --git a/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt b/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt index 4125db242..583164bfc 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt @@ -35,18 +35,6 @@ sealed interface Action { override val icon: Int = R.drawable.rounded_mobile_vibrate_24 } - @Keep - data object ShowNotification : Action { - override val title: Int = R.string.diy_action_notification - override val icon: Int = R.drawable.rounded_notifications_unread_24 - } - - @Keep - data object RemoveNotification : Action { - override val title: Int = R.string.diy_action_remove_notification - override val icon: Int = R.drawable.rounded_notifications_off_24 - } - @Keep data object TurnOnFlashlight : Action { override val title: Int = R.string.diy_action_flashlight_on @@ -177,6 +165,27 @@ sealed interface Action { override val icon: Int = R.drawable.rounded_mobile_sound_24 } + @Keep + data object CycleSoundModes : Action { + override val title: Int = R.string.diy_action_cycle_sound_modes + override val icon: Int = R.drawable.rounded_volume_up_24 + override val permissions: List = listOf("NOTIFICATION_POLICY") + } + + @Keep + data object ToggleMute : Action { + override val title: Int = R.string.diy_action_toggle_mute + override val icon: Int = R.drawable.rounded_volume_off_24 + override val permissions: List = listOf("NOTIFICATION_POLICY") + } + + @Keep + data object ToggleVibrate : Action { + override val title: Int = R.string.diy_action_toggle_vibrate + override val icon: Int = R.drawable.rounded_mobile_vibrate_24 + override val permissions: List = listOf("NOTIFICATION_POLICY") + } + @Keep data object LikeCurrentSong : Action { override val title: Int = R.string.diy_action_like_current_song diff --git a/app/src/main/java/com/sameerasw/essentials/domain/diy/ActionGsonAdapter.kt b/app/src/main/java/com/sameerasw/essentials/domain/diy/ActionGsonAdapter.kt new file mode 100644 index 000000000..5b1df8d12 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/domain/diy/ActionGsonAdapter.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2026 sameerasw.com + * License: MIT License + * + * Feature Module: Domain Layer Models & Registries + * File: ActionGsonAdapter.kt + * Description: Shared Gson serializer/deserializer for the Action sealed interface. + * Used by both DIYRepository and SettingsRepository. + */ + +package com.sameerasw.essentials.domain.diy + +import com.google.gson.GsonBuilder +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonSerializationContext +import com.google.gson.JsonSerializer +import kotlin.reflect.KClass + +object ActionGsonAdapter { + + private class SealedAdapter(private val kClass: KClass) : + JsonSerializer, JsonDeserializer { + + override fun serialize( + src: T, + typeOfSrc: java.lang.reflect.Type, + context: JsonSerializationContext + ): JsonElement { + val element = context.serialize(src) + if (element.isJsonObject) { + element.asJsonObject.addProperty("type", src::class.simpleName) + } + return element + } + + override fun deserialize( + json: JsonElement, + typeOfT: java.lang.reflect.Type, + context: JsonDeserializationContext + ): T? { + val typeName = json.asJsonObject.get("type")?.asString ?: return null + val subClass = kClass.sealedSubclasses.firstOrNull { it.simpleName == typeName } + return if (subClass != null) { + if (subClass.objectInstance != null) subClass.objectInstance + else context.deserialize(json, subClass.java) + } else { + null + } + } + } + + private val gson = GsonBuilder() + .registerTypeAdapter(Action::class.java, SealedAdapter(Action::class)) + .create() + + fun toJson(action: Action): String = gson.toJson(action, Action::class.java) + + fun fromJson(json: String): Action? = try { + gson.fromJson(json, Action::class.java) + } catch (_: Exception) { + null + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/domain/diy/ActionRegistry.kt b/app/src/main/java/com/sameerasw/essentials/domain/diy/ActionRegistry.kt new file mode 100644 index 000000000..818c013b4 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/domain/diy/ActionRegistry.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2026 sameerasw.com + * License: MIT License + * + * Feature Module: Domain Layer Models & Registries + * File: ActionRegistry.kt + * Description: Central registry providing categorised action lists for both DIY automation and Button Remap. + */ + +package com.sameerasw.essentials.domain.diy + +import android.os.Build +import com.sameerasw.essentials.R + +object ActionRegistry { + + data class ActionCategory( + val titleRes: Int, + val actions: List + ) + + /** + * Returns all action categories available for the given context. + * + * @param sdkInt Current SDK level, used to gate Android-version-specific actions. + * @param screenOnOnly When true, actions that only make sense with the screen on (e.g. screenshot) are included; + * when false they are excluded. Used by Button Remap screen-off tab. + */ + fun getCategories( + sdkInt: Int = Build.VERSION.SDK_INT, + screenOnOnly: Boolean? = null + ): List { + val connectivityActions = listOf( + Action.TurnOnWifi, + Action.TurnOffWifi, + Action.TurnOnCellularData, + Action.TurnOffCellularData, + Action.TurnOnHotspot, + Action.TurnOffHotspot, + Action.ToggleHotspot + ) + + val displayActions = buildList { + add(Action.TurnOnAutoBrightness) + add(Action.TurnOffAutoBrightness) + add(Action.DimWallpaper()) + add(Action.ScreenOff()) + if (sdkInt >= 35) add(Action.DeviceEffects()) + } + + val appsActions = listOf( + Action.OpenApp(), + Action.AIAssistant, + Action.FreezeApps(), + Action.UnfreezeApps(), + Action.FreezeTag(), + Action.PinApp, + Action.Keyboard() + ) + + val systemActions = buildList { + add(Action.TurnOnFlashlight) + add(Action.TurnOffFlashlight) + add(Action.ToggleFlashlight) + add(Action.TurnOnLowPower) + add(Action.TurnOffLowPower) + add(Action.CustomSettings()) + add(Action.CircleToSearch) + // TakeScreenshot only available on screen-on context (null means no filter = include always) + if (screenOnOnly == null || screenOnOnly == true) { + add(Action.TakeScreenshot) + } + } + + val soundMediaActions = listOf( + Action.SoundMode(), + Action.CycleSoundModes, + Action.ToggleMute, + Action.ToggleVibrate, + Action.HapticVibration, + Action.ToggleMediaVolume, + Action.MediaPlayPause, + Action.MediaNext, + Action.MediaPrevious, + Action.LikeCurrentSong + ) + + val essentialsActions = listOf( + Action.SometimesEssentials() + ) + + return listOf( + ActionCategory(R.string.diy_category_connectivity, connectivityActions), + ActionCategory(R.string.diy_category_display, displayActions), + ActionCategory(R.string.diy_category_apps, appsActions), + ActionCategory(R.string.diy_category_system, systemActions), + ActionCategory(R.string.diy_category_sound_media, soundMediaActions), + ActionCategory(R.string.diy_category_essentials, essentialsActions) + ) + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/services/InputEventListenerService.kt b/app/src/main/java/com/sameerasw/essentials/services/InputEventListenerService.kt index 17d84b4b3..15e6e84d3 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/InputEventListenerService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/InputEventListenerService.kt @@ -15,6 +15,7 @@ import android.os.IBinder import android.util.Log import android.view.Display import androidx.core.app.NotificationCompat +import com.sameerasw.essentials.data.repository.SettingsRepository import com.sameerasw.essentials.input.InputDeviceScanner import com.sameerasw.essentials.input.VolumeLongPressDetector import com.sameerasw.essentials.input.VolumePressEvent @@ -175,11 +176,13 @@ class InputEventListenerService : Service() { val pm = getSystemService(POWER_SERVICE) as android.os.PowerManager val isScreenOn = pm.isInteractive - val suffix = if (isScreenOn) "_on" else "_off" - val key = - if (event.direction == com.sameerasw.essentials.input.VolumeDirection.UP) "button_remap_vol_up_action$suffix" else "button_remap_vol_down_action$suffix" - val actionStr = prefs.getString(key, "None") - if (actionStr != "None") { + val key = if (event.direction == com.sameerasw.essentials.input.VolumeDirection.UP) { + if (isScreenOn) SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_ON else SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_OFF + } else { + if (isScreenOn) SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_ON else SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_OFF + } + val action = SettingsRepository(this@InputEventListenerService).getRemapAction(key) + if (action != null) { val am = getSystemService(AUDIO_SERVICE) as android.media.AudioManager val direction = @@ -195,19 +198,17 @@ class InputEventListenerService : Service() { } } } else { - val prefs = getSharedPreferences( - "essentials_prefs", - MODE_PRIVATE - ) val pm = getSystemService(POWER_SERVICE) as android.os.PowerManager val isScreenOn = pm.isInteractive - val suffix = if (isScreenOn) "_on" else "_off" - val key = - if (event.direction == com.sameerasw.essentials.input.VolumeDirection.UP) "button_remap_vol_up_action$suffix" else "button_remap_vol_down_action$suffix" - val actionStr = prefs.getString(key, "None") + val key = if (event.direction == com.sameerasw.essentials.input.VolumeDirection.UP) { + if (isScreenOn) SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_ON else SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_OFF + } else { + if (isScreenOn) SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_ON else SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_OFF + } + val action = SettingsRepository(this@InputEventListenerService).getRemapAction(key) - if (actionStr != "None") { + if (action != null) { val am = getSystemService(AUDIO_SERVICE) as android.media.AudioManager val dirKey = diff --git a/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt index 3ac2e50d9..8b481b6f2 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt @@ -84,14 +84,6 @@ object CombinedActionExecutor { } } - is Action.ShowNotification -> { - // Placeholder - } - - is Action.RemoveNotification -> { - // Placeholder - } - is Action.DimWallpaper -> { com.sameerasw.essentials.utils.ShellUtils.runCommand( context, @@ -332,6 +324,40 @@ object CombinedActionExecutor { } } + is Action.CycleSoundModes -> { + com.sameerasw.essentials.services.handlers.SoundModeHandler(context).cycleNextMode() + } + + is Action.ToggleMute -> { + val am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager + if (nm.isNotificationPolicyAccessGranted) { + try { + am.ringerMode = if (am.ringerMode == AudioManager.RINGER_MODE_SILENT) + AudioManager.RINGER_MODE_NORMAL + else + AudioManager.RINGER_MODE_SILENT + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + is Action.ToggleVibrate -> { + val am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager + if (nm.isNotificationPolicyAccessGranted) { + try { + am.ringerMode = if (am.ringerMode == AudioManager.RINGER_MODE_VIBRATE) + AudioManager.RINGER_MODE_NORMAL + else + AudioManager.RINGER_MODE_VIBRATE + } catch (e: Exception) { + e.printStackTrace() + } + } + } + is Action.LikeCurrentSong -> { context.sendBroadcast( Intent("com.sameerasw.essentials.ACTION_LIKE_CURRENT_SONG").setPackage( diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/ButtonRemapHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/ButtonRemapHandler.kt index 47026ba56..fde26b1a5 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/ButtonRemapHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/ButtonRemapHandler.kt @@ -21,19 +21,27 @@ import android.os.Vibrator import android.os.VibratorManager import android.util.Log import android.view.KeyEvent +import com.sameerasw.essentials.data.repository.SettingsRepository import com.sameerasw.essentials.domain.HapticFeedbackType +import com.sameerasw.essentials.domain.diy.Action import com.sameerasw.essentials.services.InputEventListenerService +import com.sameerasw.essentials.services.automation.executors.CombinedActionExecutor import com.sameerasw.essentials.utils.performHapticFeedback +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch class ButtonRemapHandler( private val service: AccessibilityService, private val flashlightHandler: FlashlightHandler ) { - private val soundModeHandler = SoundModeHandler(service) + private val settingsRepository = SettingsRepository(service) private val handler = Handler(Looper.getMainLooper()) + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private var isLongPressTriggered: Boolean = false private var lastPressedKeyCode: Int = -1 - private var lastPendingAction: String? = null + private var lastPendingAction: Action? = null private val longPressTimeout = 500L private val longPressRunnable = Runnable { @@ -82,8 +90,8 @@ class ButtonRemapHandler( val suffix = "_off" val actionKey = if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) "button_remap_vol_up_action$suffix" else "button_remap_vol_down_action$suffix" - val action = prefs.getString(actionKey, "None") - val isMapped = action != null && action != "None" + val action = settingsRepository.getRemapAction(actionKey) + val isMapped = action != null if (isMapped || isTorchControl) { return true @@ -95,38 +103,26 @@ class ButtonRemapHandler( val isAlwaysTurnOffEnabled = prefs.getBoolean("flashlight_always_turn_off_enabled", false) val isVolUpFlashlight = - prefs.getString("button_remap_vol_up_action_off", "None") == "Toggle flashlight" || - prefs.getString( - "button_remap_vol_up_action_on", - "None" - ) == "Toggle flashlight" + settingsRepository.getRemapAction(SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_OFF) is Action.ToggleFlashlight || + settingsRepository.getRemapAction(SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_ON) is Action.ToggleFlashlight val isVolDownFlashlight = - prefs.getString( - "button_remap_vol_down_action_off", - "None" - ) == "Toggle flashlight" || - prefs.getString( - "button_remap_vol_down_action_on", - "None" - ) == "Toggle flashlight" + settingsRepository.getRemapAction(SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_OFF) is Action.ToggleFlashlight || + settingsRepository.getRemapAction(SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_ON) is Action.ToggleFlashlight val isFlashlightCapableButton = (keyCode == KeyEvent.KEYCODE_VOLUME_UP && isVolUpFlashlight) || (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && isVolDownFlashlight) - val actionKeySuffix = if (isScreenInteractive) "_on" else "_off" val actionKey = if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { - "button_remap_vol_up_action$actionKeySuffix" + if (isScreenInteractive) SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_ON else SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_OFF } else { - "button_remap_vol_down_action$actionKeySuffix" + if (isScreenInteractive) SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_ON else SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_OFF } - val mappedAction = prefs.getString(actionKey, "None") ?: "None" + val mappedAction = settingsRepository.getRemapAction(actionKey) - val targetLongPressAction = if (isAlwaysTurnOffEnabled && isFlashlightCapableButton) { - "Toggle flashlight" - } else if (mappedAction != "None") { - mappedAction + val targetLongPressAction: Action = if (isAlwaysTurnOffEnabled && isFlashlightCapableButton) { + Action.ToggleFlashlight } else { - "Toggle flashlight" + mappedAction ?: Action.ToggleFlashlight } if (event.action == KeyEvent.ACTION_DOWN) { @@ -150,25 +146,21 @@ class ButtonRemapHandler( val isScreenOn = isScreenInteractive - val actionKeySuffix = if (isScreenOn) "_on" else "_off" val actionKey = if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { - "button_remap_vol_up_action$actionKeySuffix" + if (isScreenOn) SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_ON else SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_OFF } else { - "button_remap_vol_down_action$actionKeySuffix" + if (isScreenOn) SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_ON else SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_OFF } - val action = prefs.getString(actionKey, "None") ?: "None" + val action = settingsRepository.getRemapAction(actionKey) val isAlwaysTurnOffEnabled = prefs.getBoolean("flashlight_always_turn_off_enabled", false) val isVolUpFlashlight = - prefs.getString("button_remap_vol_up_action_off", "None") == "Toggle flashlight" || - prefs.getString("button_remap_vol_up_action_on", "None") == "Toggle flashlight" + settingsRepository.getRemapAction(SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_OFF) is Action.ToggleFlashlight || + settingsRepository.getRemapAction(SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_ON) is Action.ToggleFlashlight val isVolDownFlashlight = - prefs.getString("button_remap_vol_down_action_off", "None") == "Toggle flashlight" || - prefs.getString( - "button_remap_vol_down_action_on", - "None" - ) == "Toggle flashlight" + settingsRepository.getRemapAction(SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_OFF) is Action.ToggleFlashlight || + settingsRepository.getRemapAction(SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_ON) is Action.ToggleFlashlight val isFlashlightCapableButton = (keyCode == KeyEvent.KEYCODE_VOLUME_UP && isVolUpFlashlight) || @@ -176,10 +168,10 @@ class ButtonRemapHandler( var finalAction = action if (flashlightHandler.isTorchOn && isAlwaysTurnOffEnabled && isFlashlightCapableButton) { - finalAction = "Toggle flashlight" + finalAction = Action.ToggleFlashlight } - if (finalAction == "None") return false + if (finalAction == null) return false if (event.action == KeyEvent.ACTION_DOWN) { if (event.repeatCount == 0) { @@ -212,127 +204,36 @@ class ButtonRemapHandler( if (intent.action == InputEventListenerService.ACTION_VOLUME_LONG_PRESSED) { val direction = intent.getStringExtra(InputEventListenerService.EXTRA_DIRECTION) if (direction != null) { - val prefs = service.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) val isScreenOn = try { (service.getSystemService(Context.POWER_SERVICE) as PowerManager).isInteractive } catch (e: Exception) { false } - val actionKeySuffix = if (isScreenOn) "_on" else "_off" val actionKey = if (direction == "UP") { - "button_remap_vol_up_action$actionKeySuffix" + if (isScreenOn) SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_ON else SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_OFF } else { - "button_remap_vol_down_action$actionKeySuffix" + if (isScreenOn) SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_ON else SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_OFF + } + val action = settingsRepository.getRemapAction(actionKey) + if (action != null) { + handleLongPress(action) } - val action = prefs.getString(actionKey, "None") ?: "None" - handleLongPress(action) - } - } - } - - private fun handleLongPress(action: String) { - when (action) { - "Toggle flashlight" -> flashlightHandler.toggleFlashlight() - "Media play/pause" -> sendMediaKey(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) - "Media next" -> sendMediaKey(KeyEvent.KEYCODE_MEDIA_NEXT) - "Media previous" -> sendMediaKey(KeyEvent.KEYCODE_MEDIA_PREVIOUS) - "Toggle vibrate" -> toggleRingerMode(AudioManager.RINGER_MODE_VIBRATE) - "Toggle mute" -> toggleRingerMode(AudioManager.RINGER_MODE_SILENT) - "AI assistant" -> launchAssistant() - "Take screenshot" -> takeScreenshot() - "Cycle sound modes" -> cycleSoundModes() - "Toggle media volume" -> toggleMediaVolume() - "Like current song" -> { - service.sendBroadcast( - Intent("com.sameerasw.essentials.ACTION_LIKE_CURRENT_SONG").setPackage( - service.packageName - ) - ) - triggerHapticFeedback() - } - - "Circle to Search" -> { - com.sameerasw.essentials.utils.OmniTriggerUtil.trigger(service) - triggerHapticFeedback() } } } - private fun cycleSoundModes() { - soundModeHandler.cycleNextMode() - triggerHapticFeedback() - } - - private fun takeScreenshot() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT) - triggerHapticFeedback() - } else { - Log.w("ButtonRemap", "Take screenshot is only supported on Android 9+") - } - } - - private fun sendMediaKey(keyCode: Int) { - val am = service.getSystemService(Context.AUDIO_SERVICE) as AudioManager - am.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode)) - am.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, keyCode)) - triggerHapticFeedback() - } - - private fun toggleMediaVolume() { - val am = service.getSystemService(Context.AUDIO_SERVICE) as AudioManager - val currentVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC) - val prefs = service.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) - - if (currentVolume > 0) { - // Mute and save current volume - prefs.edit().putInt("last_media_volume", currentVolume).apply() - am.setStreamVolume(AudioManager.STREAM_MUSIC, 0, AudioManager.FLAG_SHOW_UI) + private fun handleLongPress(action: Action) { + if (action is Action.ToggleFlashlight) { + flashlightHandler.toggleFlashlight() } else { - // Restore last known volume or default to mid-range - val lastVolume = prefs.getInt( - "last_media_volume", - am.getStreamMaxVolume(AudioManager.STREAM_MUSIC) / 2 - ) - am.setStreamVolume(AudioManager.STREAM_MUSIC, lastVolume, AudioManager.FLAG_SHOW_UI) - } - triggerHapticFeedback() - } - - private fun toggleRingerMode(targetMode: Int) { - val notificationManager = - service.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager - if (!notificationManager.isNotificationPolicyAccessGranted) { - return - } - - val am = service.getSystemService(Context.AUDIO_SERVICE) as AudioManager - val currentMode = am.ringerMode - - try { - if (currentMode == targetMode) { - am.ringerMode = AudioManager.RINGER_MODE_NORMAL - } else { - am.ringerMode = targetMode + scope.launch { + CombinedActionExecutor.execute(service, action) } triggerHapticFeedback() - } catch (e: Exception) { - Log.e("ButtonRemap", "Error toggling ringer mode", e) } } - private fun launchAssistant() { - try { - val intent = Intent(Intent.ACTION_VOICE_COMMAND).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - service.startActivity(intent) - triggerHapticFeedback() - } catch (e: Exception) { - Log.e("ButtonRemap", "Failed to launch assistant", e) - } - } private fun triggerHapticFeedback() { try { 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 417f534a0..11cf3d141 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 @@ -73,6 +73,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.sameerasw.essentials.R import com.sameerasw.essentials.domain.diy.Action +import com.sameerasw.essentials.domain.diy.ActionRegistry import com.sameerasw.essentials.domain.diy.Automation import com.sameerasw.essentials.domain.diy.DIYRepository import com.sameerasw.essentials.domain.diy.Trigger @@ -884,67 +885,7 @@ class AutomationEditorActivity : ComponentActivity() { } val actionCategories = remember(currentSelection) { - val connectivityActions = listOf( - Action.TurnOnWifi, - Action.TurnOffWifi, - Action.TurnOnCellularData, - Action.TurnOffCellularData, - Action.TurnOnHotspot, - Action.TurnOffHotspot, - Action.ToggleHotspot - ) - val displayActions = mutableListOf( - Action.TurnOnAutoBrightness, - Action.TurnOffAutoBrightness, - Action.DimWallpaper(), - Action.ScreenOff() - ).apply { - if (android.os.Build.VERSION.SDK_INT >= 35) { - add(Action.DeviceEffects()) - } - } - val appsActions = listOf( - Action.OpenApp(), - Action.AIAssistant, - Action.FreezeApps(), - Action.UnfreezeApps(), - Action.FreezeTag(), - Action.PinApp, - Action.Keyboard() - ) - val systemActions = listOf( - Action.TurnOnFlashlight, - Action.TurnOffFlashlight, - Action.ToggleFlashlight, - Action.TurnOnLowPower, - Action.TurnOffLowPower, - Action.CustomSettings(), - Action.CircleToSearch, - Action.TakeScreenshot, - Action.ShowNotification, - Action.RemoveNotification - ) - val soundMediaActions = listOf( - Action.SoundMode(), - Action.HapticVibration, - Action.ToggleMediaVolume, - Action.MediaPlayPause, - Action.MediaNext, - Action.MediaPrevious, - Action.LikeCurrentSong - ) - val essentialsActions = listOf( - Action.SometimesEssentials() - ) - - listOf( - R.string.diy_category_connectivity to connectivityActions, - R.string.diy_category_display to displayActions, - R.string.diy_category_apps to appsActions, - R.string.diy_category_system to systemActions, - R.string.diy_category_sound_media to soundMediaActions, - R.string.diy_category_essentials to essentialsActions - ) + ActionRegistry.getCategories().map { it.titleRes to it.actions } } var expandedActionCategory by remember { diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/hardware/ButtonRemapSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/hardware/ButtonRemapSettingsUI.kt index 92f7c59fb..33240ae95 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/hardware/ButtonRemapSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/hardware/ButtonRemapSettingsUI.kt @@ -4,7 +4,7 @@ * * Feature Module: Hardware Features * File: ButtonRemapSettingsUI.kt - * Description: Composable screen for remapping volume and hardware keys. + * Description: Composable screen for remapping volume and hardware keys with unified DIY actions. */ package com.sameerasw.essentials.ui.features.system @@ -50,12 +50,23 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import com.sameerasw.essentials.R import com.sameerasw.essentials.domain.HapticFeedbackType +import com.sameerasw.essentials.domain.diy.Action +import com.sameerasw.essentials.domain.diy.ActionRegistry +import com.sameerasw.essentials.domain.model.AppSelection import com.sameerasw.essentials.shizuku.ShizukuPermissionHelper import com.sameerasw.essentials.shizuku.ShizukuStatus +import com.sameerasw.essentials.ui.components.CategoryExpandableSection import com.sameerasw.essentials.ui.core.cards.IconToggleItem import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer import com.sameerasw.essentials.ui.core.pickers.HapticFeedbackPicker import com.sameerasw.essentials.ui.core.pickers.SegmentedPicker +import com.sameerasw.essentials.ui.core.sheets.AppSelectionSheet +import com.sameerasw.essentials.ui.core.sheets.CustomSettingsSheet +import com.sameerasw.essentials.ui.core.sheets.DimWallpaperSettingsSheet +import com.sameerasw.essentials.ui.core.sheets.ScreenOffSettingsSheet +import com.sameerasw.essentials.ui.core.sheets.SingleAppSelectionSheet +import com.sameerasw.essentials.ui.core.sheets.SoundModeSettingsSheet +import com.sameerasw.essentials.ui.features.apps.sheets.KeyboardSelectionSheet import com.sameerasw.essentials.ui.modifiers.highlight import com.sameerasw.essentials.utils.HapticUtil import com.sameerasw.essentials.viewmodels.MainViewModel @@ -68,20 +79,30 @@ fun ButtonRemapSettingsUI( highlightSetting: String? = null ) { val context = LocalContext.current - val showLikeSongOptions = remember { mutableStateOf(false) } - - if (showLikeSongOptions.value) { - LikeSongSettingsSheet( - onDismiss = { showLikeSongOptions.value = false }, - viewModel = viewModel, - context = context - ) - } - val view = LocalView.current var selectedScreenTab by remember { mutableIntStateOf(0) } // 0: Off, 1: On var selectedButtonTab by remember { mutableIntStateOf(0) } // 0: Up, 1: Down var showFlashlightOptions by remember { mutableStateOf(false) } + val showLikeSongOptions = remember { mutableStateOf(false) } + + // Action Config Sheets State + var showDimSettings by remember { mutableStateOf(false) } + var showScreenOffSettings by remember { mutableStateOf(false) } + var showDeviceEffectsSettings by remember { mutableStateOf(false) } + var showSoundModeSettings by remember { mutableStateOf(false) } + var showSometimesEssentialsSettings by remember { mutableStateOf(false) } + var showFreezeTagSettings by remember { mutableStateOf(false) } + var showOpenAppSettings by remember { mutableStateOf(false) } + var showFreezeAppsSettings by remember { mutableStateOf(false) } + var temporarySelectedAppsForAction by remember { mutableStateOf>(emptyList()) } + var showSetKeyboardSheet by remember { mutableStateOf(false) } + var showCustomSettingsSettings by remember { mutableStateOf(false) } + var configAction by remember { mutableStateOf(null) } + + // Missing permission handling sheet + var showPermissionSheet by remember { mutableStateOf(false) } + var permissionKeysToShow by remember { mutableStateOf>(emptyList()) } + var permissionFeatureTitle by remember { mutableStateOf("") } val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current var shizukuStatus by remember { mutableStateOf(ShizukuStatus.NOT_RUNNING) } @@ -100,6 +121,44 @@ fun ButtonRemapSettingsUI( } } + fun getMissingPermissionsHelper(action: Action?): List { + if (action == null) return emptyList() + val resolvedPermissions = action.permissions.map { permKey -> + if (permKey == "SHIZUKU" || permKey == "ROOT") { + if (com.sameerasw.essentials.utils.ShellUtils.isRootEnabled(context)) "ROOT" else "SHIZUKU" + } else { + permKey + } + }.distinct() + + return resolvedPermissions.filter { permKey -> + when (permKey) { + "SHIZUKU" -> !viewModel.isShizukuPermissionGranted.value + "ROOT" -> !viewModel.isRootPermissionGranted.value + "WRITE_SETTINGS" -> !viewModel.isWriteSettingsEnabled.value + "NOTIFICATION_POLICY" -> !viewModel.isNotificationPolicyAccessGranted.value + "WRITE_SECURE_SETTINGS" -> !viewModel.isWriteSecureSettingsEnabled.value + else -> false + } + } + } + + val currentAction: Action? = when (selectedScreenTab) { + 0 if selectedButtonTab == 0 -> viewModel.volumeUpActionOff.value + 0 if selectedButtonTab == 1 -> viewModel.volumeDownActionOff.value + 1 if selectedButtonTab == 0 -> viewModel.volumeUpActionOn.value + else -> viewModel.volumeDownActionOn.value + } + + val onActionSelected: (Action?) -> Unit = { action -> + when (selectedScreenTab) { + 0 if selectedButtonTab == 0 -> viewModel.setVolumeUpActionOff(action, context) + 0 if selectedButtonTab == 1 -> viewModel.setVolumeDownActionOff(action, context) + 1 if selectedButtonTab == 0 -> viewModel.setVolumeUpActionOn(action, context) + else -> viewModel.setVolumeDownActionOn(action, context) + } + } + Column( modifier = modifier .fillMaxSize() @@ -139,21 +198,18 @@ fun ButtonRemapSettingsUI( if (shellHasPermission) { viewModel.setButtonRemapUseShizuku(true, context) } else if (shellIsAvailable && !isRootEnabled) { - // Shizuku logic shizukuHelper.requestPermission { _, grantResult -> if (grantResult == android.content.pm.PackageManager.PERMISSION_GRANTED) { viewModel.setButtonRemapUseShizuku(true, context) } } } else if (isRootEnabled && !shellHasPermission) { - // Root logic viewModel.setButtonRemapUseShizuku(true, context) com.sameerasw.essentials.utils.ShellUtils.runCommand( context, "id" ) } else { - // Provider not running viewModel.setButtonRemapUseShizuku(true, context) val toastRes = if (isRootEnabled) R.string.root_not_available_toast else R.string.shizuku_not_running_toast @@ -175,7 +231,6 @@ fun ButtonRemapSettingsUI( enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut() ) { - // Status indicator Row( modifier = Modifier .fillMaxWidth() @@ -250,8 +305,6 @@ fun ButtonRemapSettingsUI( exit = shrinkVertically() + fadeOut() ) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - // Flashlight Options - // Haptic Feedback (Common) Text( text = stringResource(R.string.settings_section_haptic), @@ -279,7 +332,7 @@ fun ButtonRemapSettingsUI( color = MaterialTheme.colorScheme.onSurfaceVariant ) - // Button Picker & Actions + // Button Picker & Tabs RoundedCardContainer(spacing = 2.dp) { val screenOptions = listOf( stringResource(R.string.screen_off), @@ -307,119 +360,103 @@ fun ButtonRemapSettingsUI( }, labelProvider = { it } ) + } - val currentAction = when (selectedScreenTab) { - 0 if selectedButtonTab == 0 -> viewModel.volumeUpActionOff.value - 0 if selectedButtonTab == 1 -> viewModel.volumeDownActionOff.value - 1 if selectedButtonTab == 0 -> viewModel.volumeUpActionOn.value - else -> viewModel.volumeDownActionOn.value - } - - val onActionSelected: (String) -> Unit = { action -> - when (selectedScreenTab) { - 0 if selectedButtonTab == 0 -> viewModel.setVolumeUpActionOff( - action, - context - ) - - 0 if selectedButtonTab == 1 -> viewModel.setVolumeDownActionOff( - action, - context - ) - - 1 if selectedButtonTab == 0 -> viewModel.setVolumeUpActionOn( - action, - context - ) - - else -> viewModel.setVolumeDownActionOn(action, context) - } - } - + // None Option + RoundedCardContainer(spacing = 2.dp) { RemapActionItem( title = stringResource(R.string.haptic_none), - isSelected = currentAction == "None", - onClick = { onActionSelected("None") }, + isSelected = currentAction == null, + onClick = { onActionSelected(null) }, iconRes = R.drawable.rounded_do_not_disturb_on_24, ) - RemapActionItem( - title = stringResource(R.string.action_toggle_flashlight), - isSelected = currentAction == "Toggle flashlight", - onClick = { onActionSelected("Toggle flashlight") }, - hasSettings = true, - onSettingsClick = { showFlashlightOptions = true }, - iconRes = R.drawable.rounded_flashlight_on_24, - modifier = Modifier.highlight(highlightSetting == "flashlight_toggle") - ) - RemapActionItem( - title = stringResource(R.string.action_media_play_pause), - isSelected = currentAction == "Media play/pause", - onClick = { onActionSelected("Media play/pause") }, - iconRes = R.drawable.rounded_play_pause_24, - ) - RemapActionItem( - title = stringResource(R.string.action_media_next), - isSelected = currentAction == "Media next", - onClick = { onActionSelected("Media next") }, - iconRes = R.drawable.rounded_skip_next_24, - ) - RemapActionItem( - title = stringResource(R.string.action_media_previous), - isSelected = currentAction == "Media previous", - onClick = { onActionSelected("Media previous") }, - iconRes = R.drawable.rounded_skip_previous_24, - ) - RemapActionItem( - title = stringResource(R.string.action_toggle_vibrate), - isSelected = currentAction == "Toggle vibrate", - onClick = { onActionSelected("Toggle vibrate") }, - iconRes = R.drawable.rounded_mobile_vibrate_24, - ) - RemapActionItem( - title = stringResource(R.string.action_toggle_mute), - isSelected = currentAction == "Toggle mute", - onClick = { onActionSelected("Toggle mute") }, - iconRes = R.drawable.rounded_volume_off_24, - ) - RemapActionItem( - title = stringResource(R.string.action_ai_assistant), - isSelected = currentAction == "AI assistant", - onClick = { onActionSelected("AI assistant") }, - iconRes = R.drawable.rounded_bubble_chart_24, - ) - RemapActionItem( - title = stringResource(R.string.action_toggle_media_volume), - isSelected = currentAction == "Toggle media volume", - onClick = { onActionSelected("Toggle media volume") }, - iconRes = R.drawable.rounded_volume_off_24, - ) - RemapActionItem( - title = stringResource(R.string.action_cycle_sound_modes), - isSelected = currentAction == "Cycle sound modes", - onClick = { onActionSelected("Cycle sound modes") }, - iconRes = R.drawable.rounded_volume_up_24, - ) - RemapActionItem( - title = stringResource(R.string.action_like_song), - isSelected = currentAction == "Like current song", - onClick = { onActionSelected("Like current song") }, - iconRes = R.drawable.rounded_favorite_24, - hasSettings = true, - onSettingsClick = { showLikeSongOptions.value = true } - ) - RemapActionItem( - title = stringResource(R.string.action_circle_to_search), - isSelected = currentAction == "Circle to Search", - onClick = { onActionSelected("Circle to Search") }, - iconRes = R.drawable.frame_inspect_24px, + } + + // Categorized Actions List + val actionCategories = remember(selectedScreenTab) { + ActionRegistry.getCategories(screenOnOnly = selectedScreenTab == 1) + } + + var expandedActionCategory by remember(selectedScreenTab, selectedButtonTab) { + mutableStateOf( + actionCategories.firstOrNull { category -> + category.actions.any { currentAction != null && it::class == currentAction::class } + }?.titleRes ?: actionCategories.firstOrNull()?.titleRes ) - if (selectedScreenTab == 1) { - RemapActionItem( - title = stringResource(R.string.action_take_screenshot), - isSelected = currentAction == "Take screenshot", - onClick = { onActionSelected("Take screenshot") }, - iconRes = R.drawable.rounded_screenshot_region_24, - ) + } + + actionCategories.forEach { category -> + CategoryExpandableSection( + title = stringResource(category.titleRes), + itemCount = category.actions.size, + isExpanded = expandedActionCategory == category.titleRes, + onToggleExpand = { + expandedActionCategory = + if (expandedActionCategory == category.titleRes) null else category.titleRes + } + ) { + category.actions.forEach { action -> + val resolvedAction = + if (currentAction != null && currentAction::class == action::class) currentAction else action + val isSelected = + currentAction != null && currentAction::class == resolvedAction::class + val missing = getMissingPermissionsHelper(resolvedAction) + + fun showMissingPermissionSheet() { + permissionKeysToShow = missing + permissionFeatureTitle = resolvedAction.title + showPermissionSheet = true + } + + RemapActionItem( + title = stringResource(resolvedAction.title), + iconRes = resolvedAction.icon, + isSelected = isSelected, + hasSettings = resolvedAction.isConfigurable || resolvedAction is Action.ToggleFlashlight || resolvedAction is Action.LikeCurrentSong, + onClick = { + onActionSelected(resolvedAction) + if (missing.isNotEmpty()) { + showMissingPermissionSheet() + } + }, + onSettingsClick = { + if (resolvedAction is Action.ToggleFlashlight) { + showFlashlightOptions = true + return@RemapActionItem + } + if (resolvedAction is Action.LikeCurrentSong) { + showLikeSongOptions.value = true + return@RemapActionItem + } + if (missing.isNotEmpty()) { + showMissingPermissionSheet() + return@RemapActionItem + } + + configAction = resolvedAction + when (resolvedAction) { + is Action.DimWallpaper -> showDimSettings = true + is Action.ScreenOff -> showScreenOffSettings = true + is Action.DeviceEffects -> showDeviceEffectsSettings = true + is Action.SoundMode -> showSoundModeSettings = true + is Action.SometimesEssentials -> showSometimesEssentialsSettings = true + is Action.FreezeTag -> showFreezeTagSettings = true + is Action.OpenApp -> showOpenAppSettings = true + is Action.FreezeApps -> { + temporarySelectedAppsForAction = resolvedAction.packageNames + showFreezeAppsSettings = true + } + is Action.UnfreezeApps -> { + temporarySelectedAppsForAction = resolvedAction.packageNames + showFreezeAppsSettings = true + } + is Action.Keyboard -> showSetKeyboardSheet = true + is Action.CustomSettings -> showCustomSettingsSettings = true + else -> {} + } + } + ) + } } } } @@ -438,7 +475,15 @@ fun ButtonRemapSettingsUI( } } - // Flashlight Options Bottom Sheet + // Config Bottom Sheets + if (showLikeSongOptions.value) { + LikeSongSettingsSheet( + onDismiss = { showLikeSongOptions.value = false }, + viewModel = viewModel, + context = context + ) + } + if (showFlashlightOptions) { ModalBottomSheet( onDismissRequest = { showFlashlightOptions = false }, @@ -472,13 +517,9 @@ fun ButtonRemapSettingsUI( description = stringResource(R.string.flashlight_always_off_desc), isChecked = viewModel.isFlashlightAlwaysTurnOffEnabled.value, onCheckedChange = { - viewModel.setFlashlightAlwaysTurnOffEnabled( - it, - context - ) + viewModel.setFlashlightAlwaysTurnOffEnabled(it, context) } ) - } Button( @@ -497,7 +538,157 @@ fun ButtonRemapSettingsUI( } } + if (showDimSettings && configAction is Action.DimWallpaper) { + DimWallpaperSettingsSheet( + initialAction = configAction as Action.DimWallpaper, + onDismiss = { showDimSettings = false }, + onSave = { newAction -> + showDimSettings = false + onActionSelected(newAction) + configAction = null + } + ) + } + if (showScreenOffSettings && configAction is Action.ScreenOff) { + ScreenOffSettingsSheet( + initialAction = configAction as Action.ScreenOff, + onDismiss = { showScreenOffSettings = false }, + onSave = { newAction -> + showScreenOffSettings = false + onActionSelected(newAction) + configAction = null + } + ) + } + + if (showDeviceEffectsSettings && configAction is Action.DeviceEffects) { + com.sameerasw.essentials.ui.core.sheets.DeviceEffectsSettingsSheet( + initialAction = configAction as Action.DeviceEffects, + onDismiss = { showDeviceEffectsSettings = false }, + onSave = { newAction -> + showDeviceEffectsSettings = false + onActionSelected(newAction) + configAction = null + } + ) + } + + if (showSoundModeSettings && configAction is Action.SoundMode) { + SoundModeSettingsSheet( + initialAction = configAction as Action.SoundMode, + onDismiss = { showSoundModeSettings = false }, + onSave = { newAction -> + showSoundModeSettings = false + onActionSelected(newAction) + configAction = null + } + ) + } + + if (showSometimesEssentialsSettings && configAction is Action.SometimesEssentials) { + com.sameerasw.essentials.ui.core.sheets.SometimesEssentialsSettingsSheet( + initialAction = configAction as Action.SometimesEssentials, + onDismiss = { showSometimesEssentialsSettings = false }, + onSave = { newAction -> + showSometimesEssentialsSettings = false + onActionSelected(newAction) + configAction = null + } + ) + } + + if (showFreezeTagSettings && configAction is Action.FreezeTag) { + val availableTags = remember { + com.sameerasw.essentials.data.repository.SettingsRepository(context).getFreezeTags() + } + com.sameerasw.essentials.ui.core.sheets.FreezeTagSettingsSheet( + initialAction = configAction as Action.FreezeTag, + availableTags = availableTags, + onDismiss = { showFreezeTagSettings = false }, + onSave = { newAction -> + showFreezeTagSettings = false + onActionSelected(newAction) + configAction = null + } + ) + } + + if (showOpenAppSettings) { + SingleAppSelectionSheet( + onDismissRequest = { showOpenAppSettings = false }, + onAppSelected = { app -> + val newAction = Action.OpenApp(packageName = app.packageName) + onActionSelected(newAction) + configAction = null + } + ) + } + + if (showFreezeAppsSettings && (configAction is Action.FreezeApps || configAction is Action.UnfreezeApps)) { + AppSelectionSheet( + onDismissRequest = { + val finalAction = when (val action = configAction) { + is Action.FreezeApps -> action.copy(packageNames = temporarySelectedAppsForAction) + is Action.UnfreezeApps -> action.copy(packageNames = temporarySelectedAppsForAction) + else -> configAction + } + if (finalAction != null) { + onActionSelected(finalAction) + } + showFreezeAppsSettings = false + configAction = null + }, + onLoadApps = { + temporarySelectedAppsForAction.map { AppSelection(it, true) } + }, + onSaveApps = { _, selections -> + temporarySelectedAppsForAction = + selections.filter { it.isEnabled }.map { it.packageName } + } + ) + } + + if (showSetKeyboardSheet && configAction is Action.Keyboard) { + KeyboardSelectionSheet( + onDismissRequest = { newIme -> + showSetKeyboardSheet = false + onActionSelected(Action.Keyboard(newIme)) + configAction = null + }, + selectedIme = (configAction as? Action.Keyboard)?.inputMethodId + ) + } + + if (showCustomSettingsSettings && configAction is Action.CustomSettings) { + CustomSettingsSheet( + initialAction = configAction as Action.CustomSettings, + onDismiss = { showCustomSettingsSettings = false }, + onSave = { newAction -> + showCustomSettingsSettings = false + onActionSelected(newAction) + configAction = null + } + ) + } + + if (showPermissionSheet) { + val permissionItems = com.sameerasw.essentials.utils.PermissionUIHelper.getPermissionItems( + permissionKeysToShow, + context, + viewModel + ) + if (permissionItems.isNotEmpty()) { + com.sameerasw.essentials.ui.core.sheets.PermissionsBottomSheet( + onDismissRequest = { + showPermissionSheet = false + permissionKeysToShow = emptyList() + }, + featureTitle = permissionFeatureTitle, + permissions = permissionItems + ) + } + } } @Composable diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/DIYViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/DIYViewModel.kt index bec2f6aa7..458b35e48 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/DIYViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/DIYViewModel.kt @@ -175,8 +175,6 @@ class DIYViewModel(application: Application) : AndroidViewModel(application) { val actions = suggestion.actionTypes.mapNotNull { actionName -> when (actionName) { "HapticVibration" -> Action.HapticVibration - "ShowNotification" -> Action.ShowNotification - "RemoveNotification" -> Action.RemoveNotification "TurnOnFlashlight" -> Action.TurnOnFlashlight "TurnOffFlashlight" -> Action.TurnOffFlashlight "ToggleFlashlight" -> Action.ToggleFlashlight 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 7f2a7004c..d03feadd7 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -46,6 +46,7 @@ import com.sameerasw.essentials.data.repository.SettingsRepository import com.sameerasw.essentials.data.repository.UpdateRepository import com.sameerasw.essentials.domain.HapticFeedbackType import com.sameerasw.essentials.domain.MapsState +import com.sameerasw.essentials.domain.diy.Action import com.sameerasw.essentials.domain.model.AppSelection import com.sameerasw.essentials.domain.model.AppStandbyInfo import com.sameerasw.essentials.domain.model.DnsPreset @@ -109,10 +110,10 @@ class MainViewModel : ViewModel() { val isButtonRemapEnabled = mutableStateOf(false) val isButtonRemapUseShizuku = mutableStateOf(false) val shizukuDetectedDevicePath = mutableStateOf(null) - val volumeUpActionOff = mutableStateOf("None") - val volumeDownActionOff = mutableStateOf("None") - val volumeUpActionOn = mutableStateOf("None") - val volumeDownActionOn = mutableStateOf("None") + val volumeUpActionOff = mutableStateOf(null) + val volumeDownActionOff = mutableStateOf(null) + val volumeUpActionOn = mutableStateOf(null) + val volumeDownActionOn = mutableStateOf(null) val remapHapticType = mutableStateOf(HapticFeedbackType.DOUBLE) val isDynamicNightLightEnabled = mutableStateOf(false) val isSmartPixelsEnabled = mutableStateOf(false) @@ -1523,30 +1524,10 @@ class MainViewModel : ViewModel() { false ) // Default false here as key check logic - volumeUpActionOff.value = settingsRepository.getString( - SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_OFF, - settingsRepository.getString( - SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION, - if (oldTrigger == "Volume Up" && hasLegacyToggle) "Toggle flashlight" else "None" - ) - ) ?: "None" - - volumeDownActionOff.value = settingsRepository.getString( - SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_OFF, - settingsRepository.getString( - SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION, - if (oldTrigger == "Volume Down" && hasLegacyToggle) "Toggle flashlight" else "None" - ) - ) ?: "None" - - volumeUpActionOn.value = settingsRepository.getString( - SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_ON, - "None" - ) ?: "None" - volumeDownActionOn.value = settingsRepository.getString( - SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_ON, - "None" - ) ?: "None" + volumeUpActionOff.value = settingsRepository.getRemapAction(SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_OFF) + volumeDownActionOff.value = settingsRepository.getRemapAction(SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_OFF) + volumeUpActionOn.value = settingsRepository.getRemapAction(SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_ON) + volumeDownActionOn.value = settingsRepository.getRemapAction(SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_ON) val hapticName = settingsRepository.getString( SettingsRepository.KEY_BUTTON_REMAP_HAPTIC_TYPE, @@ -3535,23 +3516,23 @@ class MainViewModel : ViewModel() { /** * Executes the set volume up action off operation. * - * @param action [String] Target action. + * @param action [Action?] Target action. * @param context [Context] Target context. */ - fun setVolumeUpActionOff(action: String, context: Context) { + fun setVolumeUpActionOff(action: Action?, context: Context) { volumeUpActionOff.value = action - settingsRepository.putString(SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_OFF, action) + settingsRepository.setRemapAction(SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_OFF, action) } /** * Executes the set volume down action off operation. * - * @param action [String] Target action. + * @param action [Action?] Target action. * @param context [Context] Target context. */ - fun setVolumeDownActionOff(action: String, context: Context) { + fun setVolumeDownActionOff(action: Action?, context: Context) { volumeDownActionOff.value = action - settingsRepository.putString( + settingsRepository.setRemapAction( SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_OFF, action ) @@ -3560,23 +3541,23 @@ class MainViewModel : ViewModel() { /** * Executes the set volume up action on operation. * - * @param action [String] Target action. + * @param action [Action?] Target action. * @param context [Context] Target context. */ - fun setVolumeUpActionOn(action: String, context: Context) { + fun setVolumeUpActionOn(action: Action?, context: Context) { volumeUpActionOn.value = action - settingsRepository.putString(SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_ON, action) + settingsRepository.setRemapAction(SettingsRepository.KEY_BUTTON_REMAP_VOL_UP_ACTION_ON, action) } /** * Executes the set volume down action on operation. * - * @param action [String] Target action. + * @param action [Action?] Target action. * @param context [Context] Target context. */ - fun setVolumeDownActionOn(action: String, context: Context) { + fun setVolumeDownActionOn(action: Action?, context: Context) { volumeDownActionOn.value = action - settingsRepository.putString(SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_ON, action) + settingsRepository.setRemapAction(SettingsRepository.KEY_BUTTON_REMAP_VOL_DOWN_ACTION_ON, action) } /** diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 29f120a19..8499d9ed9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -993,6 +993,9 @@ AI Assistant Take Screenshot Toggle Media Volume + Cycle Sound Modes + Toggle Mute + Toggle Vibrate Like Current Song Circle to Search Pin Foreground App From 309b4d0630365eef06df88ed527c1b272f757946 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Fri, 21 Aug 2026 16:24:31 +0530 Subject: [PATCH 3/7] feat: debugging long press bottom sheet for tile --- app/src/main/AndroidManifest.xml | 8 + .../activities/DebuggingSettingsActivity.kt | 241 ++++++++++++++++++ .../ui/activities/QSPreferencesActivity.kt | 13 +- app/src/main/res/values/strings.xml | 4 + 4 files changed, 263 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/sameerasw/essentials/ui/activities/DebuggingSettingsActivity.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2679ee383..e58f3b7dd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -387,6 +387,14 @@ android:taskAffinity="" android:theme="@style/Theme.Essentials.Translucent" /> + + Unit) { + val context = LocalContext.current + val view = LocalView.current + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + fun isUsbDebuggingEnabled(): Boolean { + return try { + Settings.Global.getInt(context.contentResolver, Settings.Global.ADB_ENABLED, 0) == 1 + } catch (_: Exception) { + false + } + } + + fun isWifiDebuggingEnabled(): Boolean { + return try { + Settings.Global.getInt(context.contentResolver, "adb_wifi_enabled", 0) == 1 + } catch (_: Exception) { + false + } + } + + fun setUsbDebuggingEnabled(enabled: Boolean) { + try { + Settings.Global.putInt( + context.contentResolver, + Settings.Global.ADB_ENABLED, + if (enabled) 1 else 0 + ) + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun setWifiDebuggingEnabled(enabled: Boolean) { + try { + Settings.Global.putInt( + context.contentResolver, + "adb_wifi_enabled", + if (enabled) 1 else 0 + ) + } catch (e: Exception) { + e.printStackTrace() + } + } + + var isUsbEnabled by remember { mutableStateOf(isUsbDebuggingEnabled()) } + var isWifiEnabled by remember { mutableStateOf(isWifiDebuggingEnabled()) } + + DisposableEffect(Unit) { + val handler = Handler(Looper.getMainLooper()) + val observer = object : ContentObserver(handler) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + super.onChange(selfChange, uri) + isUsbEnabled = isUsbDebuggingEnabled() + isWifiEnabled = isWifiDebuggingEnabled() + } + } + + val adbUri = Settings.Global.getUriFor(Settings.Global.ADB_ENABLED) + val adbWifiUri = Settings.Global.getUriFor("adb_wifi_enabled") + + if (adbUri != null) { + context.contentResolver.registerContentObserver(adbUri, false, observer) + } + if (adbWifiUri != null) { + context.contentResolver.registerContentObserver(adbWifiUri, false, observer) + } + + onDispose { + context.contentResolver.unregisterContentObserver(observer) + } + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .navigationBarsPadding() + .padding(bottom = 32.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_adb_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(28.dp) + ) + Text( + text = stringResource(R.string.tile_usb_debugging), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold + ) + } + + RoundedCardContainer(spacing = 2.dp) { + IconToggleItem( + iconRes = R.drawable.rounded_adb_24, + title = stringResource(R.string.usb_debugging_title), + description = stringResource(R.string.usb_debugging_desc), + isChecked = isUsbEnabled, + onCheckedChange = { enabled -> + HapticUtil.performUIHaptic(view) + isUsbEnabled = enabled + setUsbDebuggingEnabled(enabled) + } + ) + + IconToggleItem( + iconRes = R.drawable.rounded_android_wifi_4_bar_plus_24, + title = stringResource(R.string.wireless_debugging_title), + description = stringResource(R.string.wireless_debugging_desc), + isChecked = isWifiEnabled, + onCheckedChange = { enabled -> + HapticUtil.performUIHaptic(view) + isWifiEnabled = enabled + setWifiDebuggingEnabled(enabled) + } + ) + } + + Button( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + val devIntent = Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + context.startActivity(devIntent) + onDismiss() + }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp), + shape = MaterialTheme.shapes.extraLarge + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_mobile_code_24), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Text( + text = stringResource(R.string.tile_developer_options), + modifier = Modifier.padding(start = 8.dp) + ) + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/QSPreferencesActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/QSPreferencesActivity.kt index 0dd8b4b4c..cde04526b 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/QSPreferencesActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/QSPreferencesActivity.kt @@ -94,9 +94,16 @@ class QSPreferencesActivity : ComponentActivity() { return } - if (componentName.className == "com.sameerasw.essentials.services.tiles.DeveloperOptionsTileService" || - componentName.className == "com.sameerasw.essentials.services.tiles.UsbDebuggingTileService" - ) { + if (componentName.className == "com.sameerasw.essentials.services.tiles.UsbDebuggingTileService") { + val intent = Intent(this, DebuggingSettingsActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + startActivity(intent) + finish() + return + } + + if (componentName.className == "com.sameerasw.essentials.services.tiles.DeveloperOptionsTileService") { val devIntent = Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8499d9ed9..0f9e2db30 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -304,6 +304,10 @@ Toggle Cycle Debugging + USB Debugging + Enable Android Debug Bridge (ADB) over USB + Wireless Debugging + Enable Android Debug Bridge (ADB) over Wi-Fi Color Picker Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper From 0ff336ac7cf5eb418b3646e7cdac0b6feb455739 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Fri, 21 Aug 2026 16:32:18 +0530 Subject: [PATCH 4/7] feat: add configurable tap actions for UsbDebuggingTileService and corresponding settings UI --- .../data/repository/SettingsRepository.kt | 1 + .../services/tiles/UsbDebuggingTileService.kt | 27 +++++++++-- .../activities/DebuggingSettingsActivity.kt | 46 +++++++++++++++++++ app/src/main/res/values/strings.xml | 4 ++ 4 files changed, 74 insertions(+), 4 deletions(-) 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 858d6686b..09233b10f 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 @@ -110,6 +110,7 @@ class SettingsRepository(private val context: Context) { const val PREFS_NAME = "essentials_prefs" // Keys + const val KEY_DEBUGGING_TILE_TAP_ACTION = "debugging_tile_tap_action" const val KEY_GENAI_AUTOMATION_ENABLED = "genai_automation_enabled" const val KEY_SMART_PIXELS_ENABLED = "smart_pixels_enabled" const val KEY_SMART_PIXELS_INTENSITY = "smart_pixels_intensity" diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/UsbDebuggingTileService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/UsbDebuggingTileService.kt index e49506d50..2fb26a9f1 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/tiles/UsbDebuggingTileService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/UsbDebuggingTileService.kt @@ -56,18 +56,37 @@ class UsbDebuggingTileService : BaseTileService() { } override fun getTileState(): Int { + val prefs = getSharedPreferences("essentials_prefs", MODE_PRIVATE) + val tapAction = prefs.getString("debugging_tile_tap_action", "both") ?: "both" val usbOn = isUsbDebuggingEnabled() val wifiOn = isWifiDebuggingEnabled() - return if (usbOn && wifiOn) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE + + return when (tapAction) { + "usb" -> if (usbOn) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE + "wireless" -> if (wifiOn) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE + else -> if (usbOn && wifiOn) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE + } } override fun onTileClick() { + val prefs = getSharedPreferences("essentials_prefs", MODE_PRIVATE) + val tapAction = prefs.getString("debugging_tile_tap_action", "both") ?: "both" val usbOn = isUsbDebuggingEnabled() val wifiOn = isWifiDebuggingEnabled() - val newState = if (usbOn && wifiOn) 0 else 1 - setUsbDebuggingEnabled(newState == 1) - setWifiDebuggingEnabled(newState == 1) + when (tapAction) { + "usb" -> { + setUsbDebuggingEnabled(!usbOn) + } + "wireless" -> { + setWifiDebuggingEnabled(!wifiOn) + } + else -> { + val newState = if (usbOn && wifiOn) 0 else 1 + setUsbDebuggingEnabled(newState == 1) + setWifiDebuggingEnabled(newState == 1) + } + } } private fun isUsbDebuggingEnabled(): Boolean { diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/DebuggingSettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/DebuggingSettingsActivity.kt index ceb098da3..681bd472d 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/DebuggingSettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/DebuggingSettingsActivity.kt @@ -53,8 +53,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.sameerasw.essentials.R +import com.sameerasw.essentials.data.repository.SettingsRepository import com.sameerasw.essentials.ui.core.cards.IconToggleItem import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer +import com.sameerasw.essentials.ui.core.pickers.SegmentedPicker import com.sameerasw.essentials.ui.theme.EssentialsTheme import com.sameerasw.essentials.utils.HapticUtil import com.sameerasw.essentials.viewmodels.MainViewModel @@ -212,6 +214,50 @@ fun DebuggingSettingsOverlay(onDismiss: () -> Unit) { ) } + val settingsRepository = remember { + SettingsRepository(context) + } + var tapAction by remember { + mutableStateOf( + settingsRepository.getString( + SettingsRepository.KEY_DEBUGGING_TILE_TAP_ACTION, + "both" + ) ?: "both" + ) + } + + val tapActionOptions = listOf( + "both" to R.string.debugging_tap_action_both, + "usb" to R.string.debugging_tap_action_usb, + "wireless" to R.string.debugging_tap_action_wireless + ) + + Text( + text = stringResource(R.string.debugging_default_tap_action_title), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 4.dp) + ) + + RoundedCardContainer(spacing = 0.dp) { + SegmentedPicker( + items = tapActionOptions.map { it.first }, + selectedItem = tapAction, + onItemSelected = { selected -> + tapAction = selected + settingsRepository.putString( + SettingsRepository.KEY_DEBUGGING_TILE_TAP_ACTION, + selected + ) + }, + labelProvider = { optionKey -> + val stringRes = tapActionOptions.firstOrNull { it.first == optionKey }?.second + ?: R.string.debugging_tap_action_both + context.getString(stringRes) + } + ) + } + Button( onClick = { HapticUtil.performVirtualKeyHaptic(view) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0f9e2db30..fb5d933d8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -308,6 +308,10 @@ Enable Android Debug Bridge (ADB) over USB Wireless Debugging Enable Android Debug Bridge (ADB) over Wi-Fi + Default tap action + Both + USB + Wireless Color Picker Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper From 635a8c4fcb9b0200b288528244fcc947c1c95d03 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Fri, 21 Aug 2026 16:49:26 +0530 Subject: [PATCH 5/7] feat: #744 bulk app configuration for standby apps --- .../ui/activities/FeatureSettingsActivity.kt | 49 ++++-- .../ui/features/apps/StandbyAppsSettingsUI.kt | 157 +++++++++++++----- .../apps/sheets/StandbyAppsMoveSheet.kt | 118 +++++++++++++ .../essentials/viewmodels/MainViewModel.kt | 31 ++++ app/src/main/res/values/strings.xml | 3 + 5 files changed, 303 insertions(+), 55 deletions(-) create mode 100644 app/src/main/java/com/sameerasw/essentials/ui/features/apps/sheets/StandbyAppsMoveSheet.kt diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt index 8bc4927f1..cfa50f542 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt @@ -226,6 +226,8 @@ class FeatureSettingsActivity : AppCompatActivity() { // Permission sheet state var showPermissionSheet by remember { mutableStateOf(false) } var childFeatureForPermissions by remember { mutableStateOf(null) } + var standbyAppsSelectedPackages by remember { mutableStateOf(setOf()) } + var isStandbyMoveSheetVisible by remember { mutableStateOf(false) } val isAccessibilityEnabled by viewModel.isAccessibilityEnabled val isWriteSecureSettingsEnabled by viewModel.isWriteSecureSettingsEnabled @@ -1072,7 +1074,11 @@ class FeatureSettingsActivity : AppCompatActivity() { "Standby apps" -> { StandbyAppsSettingsUI( viewModel = viewModel, - modifier = Modifier.padding(top = 16.dp) + modifier = Modifier.padding(top = 16.dp), + selectedPackages = standbyAppsSelectedPackages, + onSelectionChange = { standbyAppsSelectedPackages = it }, + showMoveSheet = isStandbyMoveSheetVisible, + onShowMoveSheetChange = { isStandbyMoveSheetVisible = it } ) } } @@ -1088,21 +1094,40 @@ class FeatureSettingsActivity : AppCompatActivity() { } } + val isStandbyMultiSelecting = featureId == "Standby apps" && standbyAppsSelectedPackages.isNotEmpty() + EssentialsFloatingToolbar( - title = pageTitle, - isBeta = featureObj?.isBeta ?: false, - onBackClick = { finish() }, + title = if (isStandbyMultiSelecting) { + stringResource(R.string.standby_apps_selected_count, standbyAppsSelectedPackages.size) + } else { + pageTitle + }, + isBeta = if (isStandbyMultiSelecting) false else (featureObj?.isBeta ?: false), + onBackClick = { + if (isStandbyMultiSelecting) { + standbyAppsSelectedPackages = emptySet() + } else { + finish() + } + }, + fabIconRes = if (isStandbyMultiSelecting) R.drawable.rounded_mobiledata_arrows_24 else null, + fabAction = if (isStandbyMultiSelecting) { + { isStandbyMoveSheetVisible = true } + } else null, + fabContentDescription = if (isStandbyMultiSelecting) stringResource(R.string.action_move_bucket) else null, modifier = Modifier .align(Alignment.BottomCenter) .zIndex(1f), - onHelpClick = { - if (featureId == "Watch") { - showWatchInstallHelpSheet = true - } else if (hasMenu) { - selectedHelpFeature = featureObj - showHelpSheet = true - } else { - showInstructionsSheet = true + onHelpClick = if (isStandbyMultiSelecting) null else { + { + if (featureId == "Watch") { + showWatchInstallHelpSheet = true + } else if (hasMenu) { + selectedHelpFeature = featureObj + showHelpSheet = true + } else { + showInstructionsSheet = true + } } } ) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/apps/StandbyAppsSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/apps/StandbyAppsSettingsUI.kt index 5af2d5ea8..90d2f7408 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/apps/StandbyAppsSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/apps/StandbyAppsSettingsUI.kt @@ -11,6 +11,8 @@ package com.sameerasw.essentials.ui.features.system import android.content.Intent import android.net.Uri +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -18,6 +20,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material3.Checkbox import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -36,7 +39,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -44,6 +46,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner import coil.compose.AsyncImage import com.sameerasw.essentials.R import com.sameerasw.essentials.domain.model.AppStandbyInfo @@ -52,6 +55,7 @@ import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer import com.sameerasw.essentials.ui.core.sheets.PermissionItem import com.sameerasw.essentials.ui.core.sheets.PermissionsBottomSheet +import com.sameerasw.essentials.ui.features.apps.sheets.StandbyAppsMoveSheet import com.sameerasw.essentials.utils.HapticUtil import com.sameerasw.essentials.viewmodels.MainViewModel @@ -59,11 +63,21 @@ import com.sameerasw.essentials.viewmodels.MainViewModel @Composable fun StandbyAppsSettingsUI( viewModel: MainViewModel, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + selectedPackages: Set = emptySet(), + onSelectionChange: (Set) -> Unit = {}, + showMoveSheet: Boolean = false, + onShowMoveSheetChange: (Boolean) -> Unit = {} ) { val context = LocalContext.current var requestingPermissionFor by remember { mutableStateOf(false) } + val isMultiSelecting = selectedPackages.isNotEmpty() + + BackHandler(enabled = isMultiSelecting) { + onSelectionChange(emptySet()) + } + val isShizukuGranted = viewModel.isShizukuAvailable.value && viewModel.isShizukuPermissionGranted.value val isRootGranted = viewModel.isRootAvailable.value && viewModel.isRootPermissionGranted.value @@ -86,6 +100,21 @@ fun StandbyAppsSettingsUI( } } + if (showMoveSheet && isMultiSelecting) { + StandbyAppsMoveSheet( + onDismissRequest = { onShowMoveSheetChange(false) }, + onBucketSelected = { targetBucket -> + onShowMoveSheetChange(false) + if (isShellGranted) { + viewModel.setAppsStandbyBucket(selectedPackages, targetBucket, context) + onSelectionChange(emptySet()) + } else { + requestingPermissionFor = true + } + } + ) + } + if (requestingPermissionFor) { val shizukuPermission = PermissionItem( iconRes = R.drawable.rounded_adb_24, @@ -173,10 +202,21 @@ fun StandbyAppsSettingsUI( } } else { bucketApps.forEach { app -> + val isSelected = selectedPackages.contains(app.packageName) AppStandbyCardItem( app = app, currentBucket = bucketCode, isShellGranted = isShellGranted, + isMultiSelecting = isMultiSelecting, + isSelected = isSelected, + onToggleSelection = { + val newSet = if (isSelected) { + selectedPackages - app.packageName + } else { + selectedPackages + app.packageName + } + onSelectionChange(newSet) + }, onMoveBucket = { targetBucket -> if (isShellGranted) { viewModel.setAppStandbyBucket( @@ -203,6 +243,9 @@ private fun AppStandbyCardItem( app: AppStandbyInfo, currentBucket: Int, isShellGranted: Boolean, + isMultiSelecting: Boolean, + isSelected: Boolean, + onToggleSelection: () -> Unit, onMoveBucket: (Int) -> Unit ) { val view = LocalView.current @@ -235,56 +278,84 @@ private fun AppStandbyCardItem( ) }, trailingContent = { - Box { - IconButton( - onClick = { - HapticUtil.performUIHaptic(view) - if (isShellGranted) { - showMenu = true - } else { - onMoveBucket(currentBucket) + if (isMultiSelecting) { + Checkbox( + checked = isSelected, + onCheckedChange = { + HapticUtil.performVirtualKeyHaptic(view) + onToggleSelection() + } + ) + } else { + Box { + IconButton( + onClick = { + HapticUtil.performUIHaptic(view) + if (isShellGranted) { + showMenu = true + } else { + onMoveBucket(currentBucket) + } } + ) { + Icon( + painter = painterResource(R.drawable.rounded_mobiledata_arrows_24), + contentDescription = stringResource(R.string.action_move_bucket), + tint = MaterialTheme.colorScheme.primary + ) } - ) { - Icon( - painter = painterResource(R.drawable.rounded_mobiledata_arrows_24), - contentDescription = "Move bucket", - tint = MaterialTheme.colorScheme.primary - ) - } - SegmentedDropdownMenu( - expanded = showMenu, - onDismissRequest = { showMenu = false } - ) { - val menuOptions = listOf( - 10 to R.string.standby_bucket_active, - 20 to R.string.standby_bucket_working_set, - 30 to R.string.standby_bucket_frequent, - 40 to R.string.standby_bucket_rare, - 45 to R.string.standby_bucket_restricted - ) - - menuOptions.forEach { (targetBucket, titleRes) -> - val isCurrent = targetBucket == currentBucket - SegmentedDropdownMenuItem( - text = { - Text(text = stringResource(titleRes)) - }, - enabled = !isCurrent, - onClick = { - HapticUtil.performUIHaptic(view) - showMenu = false - onMoveBucket(targetBucket) - } + SegmentedDropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false } + ) { + val menuOptions = listOf( + 10 to R.string.standby_bucket_active, + 20 to R.string.standby_bucket_working_set, + 30 to R.string.standby_bucket_frequent, + 40 to R.string.standby_bucket_rare, + 45 to R.string.standby_bucket_restricted ) + + menuOptions.forEach { (targetBucket, titleRes) -> + val isCurrent = targetBucket == currentBucket + SegmentedDropdownMenuItem( + text = { + Text(text = stringResource(titleRes)) + }, + enabled = !isCurrent, + onClick = { + HapticUtil.performUIHaptic(view) + showMenu = false + onMoveBucket(targetBucket) + } + ) + } } } } }, colors = ListItemDefaults.colors( - containerColor = MaterialTheme.colorScheme.surfaceBright + containerColor = if (isSelected) { + MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.35f) + } else { + MaterialTheme.colorScheme.surfaceBright + } ), - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .combinedClickable( + onClick = { + if (isMultiSelecting) { + HapticUtil.performVirtualKeyHaptic(view) + onToggleSelection() + } + }, + onLongClick = { + HapticUtil.performHeavyHaptic(view) + onToggleSelection() + } + ) ) } + diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/apps/sheets/StandbyAppsMoveSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/apps/sheets/StandbyAppsMoveSheet.kt new file mode 100644 index 000000000..70125f908 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/apps/sheets/StandbyAppsMoveSheet.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2026 sameerasw.com + * License: MIT License + * + * Feature Module: UI Feature - Apps + * File: StandbyAppsMoveSheet.kt + * Description: UI bottom sheet for selecting a target App Standby Bucket to move selected apps. + */ + +package com.sameerasw.essentials.ui.features.apps.sheets + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.sameerasw.essentials.R +import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer +import com.sameerasw.essentials.ui.core.sheets.EssentialsBottomSheet +import com.sameerasw.essentials.utils.HapticUtil + +@Composable +fun StandbyAppsMoveSheet( + onDismissRequest: () -> Unit, + onBucketSelected: (Int) -> Unit +) { + val view = LocalView.current + + val buckets = listOf( + 10 to R.string.standby_bucket_active, + 20 to R.string.standby_bucket_working_set, + 30 to R.string.standby_bucket_frequent, + 40 to R.string.standby_bucket_rare, + 45 to R.string.standby_bucket_restricted + ) + + EssentialsBottomSheet( + onDismissRequest = onDismissRequest + ) { + Column( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.standby_apps_move_sheet_title), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.SemiBold + ) + } + + RoundedCardContainer( + spacing = 2.dp, + cornerRadius = 24.dp + ) { + LazyColumn( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + itemsIndexed(buckets) { _, (bucketCode, titleRes) -> + ListItem( + checked = false, + onCheckedChange = { + HapticUtil.performVirtualKeyHaptic(view) + onBucketSelected(bucketCode) + }, + onLongClick = null, + enabled = true, + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + leadingContent = null, + supportingContent = null, + trailingContent = { + RadioButton( + selected = false, + onClick = null + ) + }, + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + contentPadding = PaddingValues( + horizontal = 16.dp, + vertical = 16.dp + ), + content = { + Text( + text = stringResource(titleRes), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + ) + } + } + } + } + } +} 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 d03feadd7..e0945b3d3 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -2870,6 +2870,37 @@ class MainViewModel : ViewModel() { } } + /** + * Executes batch set app standby bucket operation for multiple apps. + * + * @param packageNames [Set] Target package names. + * @param targetBucket [Int] Target standby bucket code. + * @param context [Context] Context for shell execution. + */ + fun setAppsStandbyBucket(packageNames: Set, targetBucket: Int, context: Context) { + val currentList = standbyAppsList.value + val updatedList = currentList.map { app -> + if (packageNames.contains(app.packageName)) { + app.copy(bucket = targetBucket) + } else app + } + standbyAppsList.value = updatedList + + viewModelScope.launch(kotlinx.coroutines.Dispatchers.IO) { + val bucketName = when (targetBucket) { + 10 -> "active" + 20 -> "working_set" + 30 -> "frequent" + 40 -> "rare" + 45 -> "restricted" + else -> "active" + } + packageNames.forEach { pkg -> + ShellUtils.runCommand(context, "am set-standby-bucket $pkg $bucketName") + } + } + } + /** * Executes the set prefer gpu composing enabled operation. * diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fb5d933d8..0310dbd21 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1982,6 +1982,9 @@ Restricted No apps in this bucket Loading standby apps... + %1$d Apps + Move to Standby Bucket + Move to bucket Graphics Prefer GPU for screen composing Disable hardware overlays to force GPU screen compositing. Resets on device reboot. From e982ab0d65ea60f1dcf047e547f6a38d723687ed Mon Sep 17 00:00:00 2001 From: sameerasw Date: Fri, 21 Aug 2026 17:39:39 +0530 Subject: [PATCH 6/7] feat: #663 volume level set in automation action --- .../sameerasw/essentials/domain/diy/Action.kt | 39 ++++ .../essentials/domain/diy/ActionRegistry.kt | 1 + .../executors/CombinedActionExecutor.kt | 15 ++ .../ui/activities/AutomationEditorActivity.kt | 24 +++ .../ui/core/pickers/SegmentedPicker.kt | 18 +- .../audio/sheets/SetVolumeSettingsSheet.kt | 185 ++++++++++++++++++ .../hardware/ButtonRemapSettingsUI.kt | 15 ++ .../res/drawable/rounded_ring_volume_24.xml | 10 + app/src/main/res/values/strings.xml | 8 + 9 files changed, 308 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/sameerasw/essentials/ui/features/audio/sheets/SetVolumeSettingsSheet.kt create mode 100644 app/src/main/res/drawable/rounded_ring_volume_24.xml diff --git a/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt b/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt index 583164bfc..e113c889f 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt @@ -192,6 +192,45 @@ sealed interface Action { override val icon: Int = R.drawable.rounded_favorite_24 } + @Keep + enum class VolumeChannel { + @SerializedName("MUSIC") + MUSIC, + + @SerializedName("RING") + RING, + + @SerializedName("ALARM") + ALARM, + + @SerializedName("CALL") + CALL, + + @SerializedName("NOTIFICATION") + NOTIFICATION, + + @SerializedName("SYSTEM") + SYSTEM + } + + @Keep + data class SetVolume( + @SerializedName("channel") val channel: VolumeChannel = VolumeChannel.MUSIC, + @SerializedName("level") val level: Int = 50 + ) : Action { + override val title: Int get() = R.string.diy_action_set_volume + override val icon: Int + get() = when (channel) { + VolumeChannel.MUSIC -> R.drawable.rounded_music_note_24 + VolumeChannel.RING -> R.drawable.rounded_ring_volume_24 + VolumeChannel.ALARM -> R.drawable.rounded_alarm_24 + VolumeChannel.CALL -> R.drawable.rounded_call_24 + VolumeChannel.NOTIFICATION -> R.drawable.rounded_notifications_unread_24 + VolumeChannel.SYSTEM -> R.drawable.rounded_android_24 + } + override val isConfigurable: Boolean = true + } + @Keep data object CircleToSearch : Action { override val title: Int = R.string.diy_action_circle_to_search diff --git a/app/src/main/java/com/sameerasw/essentials/domain/diy/ActionRegistry.kt b/app/src/main/java/com/sameerasw/essentials/domain/diy/ActionRegistry.kt index 818c013b4..24694b8bd 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/diy/ActionRegistry.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/diy/ActionRegistry.kt @@ -79,6 +79,7 @@ object ActionRegistry { Action.ToggleVibrate, Action.HapticVibration, Action.ToggleMediaVolume, + Action.SetVolume(), Action.MediaPlayPause, Action.MediaNext, Action.MediaPrevious, diff --git a/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt index 8b481b6f2..d4a7bedc1 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt @@ -324,6 +324,21 @@ object CombinedActionExecutor { } } + is Action.SetVolume -> { + val am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + val streamType = when (action.channel) { + Action.VolumeChannel.MUSIC -> AudioManager.STREAM_MUSIC + Action.VolumeChannel.RING -> AudioManager.STREAM_RING + Action.VolumeChannel.ALARM -> AudioManager.STREAM_ALARM + Action.VolumeChannel.CALL -> AudioManager.STREAM_VOICE_CALL + Action.VolumeChannel.NOTIFICATION -> AudioManager.STREAM_NOTIFICATION + Action.VolumeChannel.SYSTEM -> AudioManager.STREAM_SYSTEM + } + val max = am.getStreamMaxVolume(streamType) + val target = (action.level / 100f * max).toInt().coerceIn(0, max) + am.setStreamVolume(streamType, target, AudioManager.FLAG_SHOW_UI) + } + is Action.CycleSoundModes -> { com.sameerasw.essentials.services.handlers.SoundModeHandler(context).cycleNextMode() } 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 11cf3d141..9c9eb7b05 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 @@ -104,6 +104,7 @@ import com.sameerasw.essentials.ui.core.sheets.ScreenOffSettingsSheet import com.sameerasw.essentials.ui.core.sheets.SingleAppSelectionSheet import com.sameerasw.essentials.ui.core.sheets.SoundModeSettingsSheet import com.sameerasw.essentials.ui.core.sheets.WifiNetworkSelectionSheet +import com.sameerasw.essentials.ui.features.audio.sheets.SetVolumeSettingsSheet import com.sameerasw.essentials.ui.features.apps.sheets.KeyboardSelectionSheet import com.sameerasw.essentials.ui.theme.EssentialsTheme import com.sameerasw.essentials.utils.AppUtil @@ -271,6 +272,7 @@ class AutomationEditorActivity : ComponentActivity() { var showWifiSettings by remember { mutableStateOf(false) } var showSetKeyboardSheet by remember { mutableStateOf(false) } var showCustomSettingsSettings by remember { mutableStateOf(false) } + var showSetVolumeSettings by remember { mutableStateOf(false) } var configAction by remember { mutableStateOf(null) } // Generic config action val isTriggerConfigured = when (val trigger = selectedTrigger) { @@ -956,6 +958,7 @@ class AutomationEditorActivity : ComponentActivity() { is Action.Keyboard -> { showSetKeyboardSheet = true } + is Action.SetVolume -> showSetVolumeSettings = true is Action.CustomSettings -> showCustomSettingsSettings = true else -> {} } @@ -1143,6 +1146,27 @@ class AutomationEditorActivity : ComponentActivity() { } ) } + if (showSetVolumeSettings && configAction is Action.SetVolume) { + SetVolumeSettingsSheet( + initialAction = configAction as Action.SetVolume, + onDismiss = { showSetVolumeSettings = false }, + onSave = { newAction -> + showSetVolumeSettings = false + when (automationType) { + Automation.Type.TRIGGER -> selectedAction = newAction + Automation.Type.ACTION_SHORTCUT, Automation.Type.PIXEL_SEARCHBAR -> selectedAction = + newAction + + Automation.Type.STATE, Automation.Type.APP -> { + if (selectedActionTab == 0) selectedInAction = newAction + else selectedOutAction = newAction + } + } + configAction = null + } + ) + } + if (showSometimesEssentialsSettings && configAction is Action.SometimesEssentials) { com.sameerasw.essentials.ui.core.sheets.SometimesEssentialsSettingsSheet( initialAction = configAction as Action.SometimesEssentials, diff --git a/app/src/main/java/com/sameerasw/essentials/ui/core/pickers/SegmentedPicker.kt b/app/src/main/java/com/sameerasw/essentials/ui/core/pickers/SegmentedPicker.kt index 0007cba89..d87cfd31d 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/core/pickers/SegmentedPicker.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/core/pickers/SegmentedPicker.kt @@ -133,14 +133,18 @@ fun SegmentedPicker( ) { if (iconProvider != null) { iconProvider(item) - Spacer(Modifier.padding(end = 8.dp)) } - Text( - label, - fontSize = dimensionResource(R.dimen.font_small).value.sp, - modifier = Modifier.basicMarquee(), - maxLines = 1 - ) + if (label.isNotEmpty()) { + if (iconProvider != null) { + Spacer(Modifier.padding(end = 8.dp)) + } + Text( + label, + fontSize = dimensionResource(R.dimen.font_small).value.sp, + modifier = Modifier.basicMarquee(), + maxLines = 1 + ) + } } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/audio/sheets/SetVolumeSettingsSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/audio/sheets/SetVolumeSettingsSheet.kt new file mode 100644 index 000000000..0198a797e --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/audio/sheets/SetVolumeSettingsSheet.kt @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2026 sameerasw.com + * License: MIT License + * + * Feature Module: UI Feature - Audio + * File: SetVolumeSettingsSheet.kt + * Description: Configuration bottom sheet for the SetVolume automation action. + * Lets the user pick a sound channel (icon-only segmented picker) + * and set the target level (0–100%) with a slider. + */ + +package com.sameerasw.essentials.ui.features.audio.sheets + +import androidx.compose.foundation.layout.Arrangement +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.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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.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 com.sameerasw.essentials.R +import com.sameerasw.essentials.domain.diy.Action +import com.sameerasw.essentials.ui.components.sliders.ConfigSliderItem +import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer +import com.sameerasw.essentials.ui.core.pickers.SegmentedPicker +import com.sameerasw.essentials.ui.core.sheets.EssentialsBottomSheet +import com.sameerasw.essentials.utils.HapticUtil + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SetVolumeSettingsSheet( + initialAction: Action.SetVolume, + onDismiss: () -> Unit, + onSave: (Action.SetVolume) -> Unit +) { + val view = LocalView.current + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + var selectedChannel by remember { mutableStateOf(initialAction.channel) } + var selectedLevel by remember { mutableIntStateOf(initialAction.level) } + + val channels = Action.VolumeChannel.entries + + EssentialsBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = stringResource(R.string.diy_action_set_volume), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + + RoundedCardContainer(spacing = 2.dp) { + SegmentedPicker( + items = channels, + selectedItem = selectedChannel, + onItemSelected = { + HapticUtil.performUIHaptic(view) + selectedChannel = it + }, + labelProvider = { "" }, + iconProvider = { channel -> + val iconRes = when (channel) { + Action.VolumeChannel.MUSIC -> R.drawable.rounded_music_note_24 + Action.VolumeChannel.RING -> R.drawable.rounded_ring_volume_24 + Action.VolumeChannel.ALARM -> R.drawable.rounded_alarm_24 + Action.VolumeChannel.CALL -> R.drawable.rounded_call_24 + Action.VolumeChannel.NOTIFICATION -> R.drawable.rounded_notifications_unread_24 + Action.VolumeChannel.SYSTEM -> R.drawable.rounded_android_24 + } + Icon( + painter = painterResource(iconRes), + contentDescription = when (channel) { + Action.VolumeChannel.MUSIC -> stringResource(R.string.diy_volume_channel_music) + Action.VolumeChannel.RING -> stringResource(R.string.diy_volume_channel_ring) + Action.VolumeChannel.ALARM -> stringResource(R.string.diy_volume_channel_alarm) + Action.VolumeChannel.CALL -> stringResource(R.string.diy_volume_channel_call) + Action.VolumeChannel.NOTIFICATION -> stringResource(R.string.diy_volume_channel_notification) + Action.VolumeChannel.SYSTEM -> stringResource(R.string.diy_volume_channel_system) + }, + modifier = Modifier.size(20.dp) + ) + }, + modifier = Modifier.fillMaxWidth() + ) + + val channelTitleRes = when (selectedChannel) { + Action.VolumeChannel.MUSIC -> R.string.diy_volume_channel_music + Action.VolumeChannel.RING -> R.string.diy_volume_channel_ring + Action.VolumeChannel.ALARM -> R.string.diy_volume_channel_alarm + Action.VolumeChannel.CALL -> R.string.diy_volume_channel_call + Action.VolumeChannel.NOTIFICATION -> R.string.diy_volume_channel_notification + Action.VolumeChannel.SYSTEM -> R.string.diy_volume_channel_system + } + val channelIconRes = when (selectedChannel) { + Action.VolumeChannel.MUSIC -> R.drawable.rounded_music_note_24 + Action.VolumeChannel.RING -> R.drawable.rounded_ring_volume_24 + Action.VolumeChannel.ALARM -> R.drawable.rounded_alarm_24 + Action.VolumeChannel.CALL -> R.drawable.rounded_call_24 + Action.VolumeChannel.NOTIFICATION -> R.drawable.rounded_notifications_unread_24 + Action.VolumeChannel.SYSTEM -> R.drawable.rounded_android_24 + } + + ConfigSliderItem( + title = stringResource(channelTitleRes), + value = selectedLevel.toFloat(), + onValueChange = { selectedLevel = it.toInt() }, + valueRange = 0f..100f, + increment = 5f, + steps = 19, + valueFormatter = { "${it.toInt()}%" }, + iconRes = channelIconRes + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.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(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) + onSave(initialAction.copy(channel = selectedChannel, level = selectedLevel)) + }, + modifier = Modifier.weight(1f) + ) { + Icon( + painter = painterResource(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/features/hardware/ButtonRemapSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/hardware/ButtonRemapSettingsUI.kt index 33240ae95..5ef2be5e3 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/hardware/ButtonRemapSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/hardware/ButtonRemapSettingsUI.kt @@ -67,6 +67,7 @@ import com.sameerasw.essentials.ui.core.sheets.ScreenOffSettingsSheet import com.sameerasw.essentials.ui.core.sheets.SingleAppSelectionSheet import com.sameerasw.essentials.ui.core.sheets.SoundModeSettingsSheet import com.sameerasw.essentials.ui.features.apps.sheets.KeyboardSelectionSheet +import com.sameerasw.essentials.ui.features.audio.sheets.SetVolumeSettingsSheet import com.sameerasw.essentials.ui.modifiers.highlight import com.sameerasw.essentials.utils.HapticUtil import com.sameerasw.essentials.viewmodels.MainViewModel @@ -97,6 +98,7 @@ fun ButtonRemapSettingsUI( var temporarySelectedAppsForAction by remember { mutableStateOf>(emptyList()) } var showSetKeyboardSheet by remember { mutableStateOf(false) } var showCustomSettingsSettings by remember { mutableStateOf(false) } + var showSetVolumeSettings by remember { mutableStateOf(false) } var configAction by remember { mutableStateOf(null) } // Missing permission handling sheet @@ -451,6 +453,7 @@ fun ButtonRemapSettingsUI( showFreezeAppsSettings = true } is Action.Keyboard -> showSetKeyboardSheet = true + is Action.SetVolume -> showSetVolumeSettings = true is Action.CustomSettings -> showCustomSettingsSettings = true else -> {} } @@ -586,6 +589,18 @@ fun ButtonRemapSettingsUI( ) } + if (showSetVolumeSettings && configAction is Action.SetVolume) { + SetVolumeSettingsSheet( + initialAction = configAction as Action.SetVolume, + onDismiss = { showSetVolumeSettings = false }, + onSave = { newAction -> + showSetVolumeSettings = false + onActionSelected(newAction) + configAction = null + } + ) + } + if (showSometimesEssentialsSettings && configAction is Action.SometimesEssentials) { com.sameerasw.essentials.ui.core.sheets.SometimesEssentialsSettingsSheet( initialAction = configAction as Action.SometimesEssentials, diff --git a/app/src/main/res/drawable/rounded_ring_volume_24.xml b/app/src/main/res/drawable/rounded_ring_volume_24.xml new file mode 100644 index 000000000..cfae08c4e --- /dev/null +++ b/app/src/main/res/drawable/rounded_ring_volume_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0310dbd21..3ad5be49e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1001,6 +1001,14 @@ AI Assistant Take Screenshot Toggle Media Volume + Set Volume + Volume Level + Music + Ring + Alarm + Call + Notification + System Cycle Sound Modes Toggle Mute Toggle Vibrate From 72b410d69c9bdc44fcb36123b5730019de01cc98 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Fri, 21 Aug 2026 18:40:08 +0530 Subject: [PATCH 7/7] feat: #569 are we there yet full screen alert --- .../data/repository/SettingsRepository.kt | 8 +++ .../services/LocationReachedService.kt | 42 +++++++++-- .../ui/activities/LocationAlarmActivity.kt | 69 ++++++++++++++++--- .../location/LocationReachedSettingsUI.kt | 22 +++++- .../essentials/viewmodels/MainViewModel.kt | 12 ++++ app/src/main/res/values/strings.xml | 2 + 6 files changed, 139 insertions(+), 16 deletions(-) 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 09233b10f..193b1d8f3 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 @@ -285,6 +285,8 @@ class SettingsRepository(private val context: Context) { const val KEY_CALENDAR_SYNC_SELECTED_CALENDARS = "calendar_sync_selected_calendars" const val KEY_CALENDAR_SYNC_PERIODIC_ENABLED = "calendar_sync_periodic_enabled" const val KEY_REMOTE_LOCK_MODE = "remote_lock_mode" // 0: Screen off, 1: Lock + const val KEY_LOCATION_REACHED_FULL_SCREEN_ALARM_ENABLED = + "location_reached_full_screen_alarm_enabled" const val KEY_GITHUB_ACCESS_TOKEN = "github_access_token" const val KEY_GITHUB_WORKFLOW_TOKEN = "github_workflow_token" @@ -2878,5 +2880,11 @@ class SettingsRepository(private val context: Context) { * @param value [Int] Target value. */ fun setLockScreenClockSeedColor(value: Int) = putInt(KEY_LOCK_SCREEN_CLOCK_SEED_COLOR, value) + + fun getLocationReachedFullScreenAlarmEnabled(): Boolean = + getBoolean(KEY_LOCATION_REACHED_FULL_SCREEN_ALARM_ENABLED, true) + + fun setLocationReachedFullScreenAlarmEnabled(value: Boolean) = + putBoolean(KEY_LOCATION_REACHED_FULL_SCREEN_ALARM_ENABLED, value) } diff --git a/app/src/main/java/com/sameerasw/essentials/services/LocationReachedService.kt b/app/src/main/java/com/sameerasw/essentials/services/LocationReachedService.kt index 1787d5adc..40af7db85 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/LocationReachedService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/LocationReachedService.kt @@ -46,6 +46,7 @@ class LocationReachedService : Service() { private var isInitialCalculationDone = false private val repository by lazy { LocationReachedRepository(this) } + private val settingsRepository by lazy { com.sameerasw.essentials.data.repository.SettingsRepository(this) } private val fusedLocationClient by lazy { LocationServices.getFusedLocationProviderClient(this) } private val notificationManager by lazy { getSystemService(NOTIFICATION_SERVICE) as NotificationManager } @@ -262,7 +263,9 @@ class LocationReachedService : Service() { val alarm = repository.getAlarms().find { it.id == activeId } saveTravelProgress(alarm != null, alarm, 100, "Arrived", "0 m") - val channelId = "location_reached_channel" + val isFullScreenAlarmEnabled = settingsRepository.getLocationReachedFullScreenAlarmEnabled() + val channelId = if (isFullScreenAlarmEnabled) "location_reached_channel_alarm" else "location_reached_channel" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( channelId, @@ -271,6 +274,16 @@ class LocationReachedService : Service() { ).apply { enableLights(true) enableVibration(true) + if (isFullScreenAlarmEnabled) { + val audioAttributes = android.media.AudioAttributes.Builder() + .setContentType(android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(android.media.AudioAttributes.USAGE_ALARM) + .build() + val alarmUri = android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_ALARM) + ?: android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_RINGTONE) + ?: android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_NOTIFICATION) + setSound(alarmUri, audioAttributes) + } } notificationManager.createNotificationChannel(channel) } @@ -279,26 +292,40 @@ class LocationReachedService : Service() { this, com.sameerasw.essentials.ui.activities.LocationAlarmActivity::class.java ).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION + flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP or + Intent.FLAG_ACTIVITY_SINGLE_TOP } val fullScreenPendingIntent = PendingIntent.getActivity( this, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) - val notification = NotificationCompat.Builder(this, channelId) + val notificationBuilder = NotificationCompat.Builder(this, channelId) .setSmallIcon(R.drawable.rounded_navigation_24) .setContentTitle(getString(R.string.location_reached_notification_title)) .setContentText(getString(R.string.location_reached_notification_desc)) .setPriority(NotificationCompat.PRIORITY_MAX) .setCategory(NotificationCompat.CATEGORY_ALARM) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setDefaults(NotificationCompat.DEFAULT_ALL) - .setFullScreenIntent(fullScreenPendingIntent, true) .setAutoCancel(true) - .build() - notificationManager.notify(ALARM_NOTIFICATION_ID, notification) + if (isFullScreenAlarmEnabled) { + notificationBuilder.setFullScreenIntent(fullScreenPendingIntent, true) + } else { + notificationBuilder.setContentIntent(fullScreenPendingIntent) + notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL) + } + + notificationManager.notify(ALARM_NOTIFICATION_ID, notificationBuilder.build()) + + if (isFullScreenAlarmEnabled) { + try { + startActivity(fullScreenIntent) + } catch (e: Exception) { + e.printStackTrace() + } + } } private fun updateNotification(distanceKm: Float?) { @@ -568,6 +595,7 @@ class LocationReachedService : Service() { stopForeground(true) } notificationManager.cancel(NOTIFICATION_ID) + notificationManager.cancel(ALARM_NOTIFICATION_ID) } catch (e: Exception) { e.printStackTrace() } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/LocationAlarmActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/LocationAlarmActivity.kt index dff843366..75913e501 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/LocationAlarmActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/LocationAlarmActivity.kt @@ -10,6 +10,11 @@ package com.sameerasw.essentials.ui.activities import android.app.KeyguardManager +import android.app.NotificationManager +import android.media.AudioAttributes +import android.media.Ringtone +import android.media.RingtoneManager +import android.net.Uri import android.os.Build import android.os.Bundle import android.os.VibrationEffect @@ -45,6 +50,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale +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 @@ -54,9 +60,12 @@ import androidx.core.view.WindowCompat import com.sameerasw.essentials.R import com.sameerasw.essentials.data.repository.LocationReachedRepository import com.sameerasw.essentials.services.LocationReachedService +import com.sameerasw.essentials.utils.HapticUtil class LocationAlarmActivity : ComponentActivity() { + private var ringtone: Ringtone? = null + override fun onCreate(savedInstanceState: Bundle?) { WindowCompat.setDecorFitsSystemWindows(window, false) enableEdgeToEdge() @@ -78,17 +87,13 @@ class LocationAlarmActivity : ComponentActivity() { } } + startAlarmRingtone() startUrgentVibration() } - override fun onStop() { - super.onStop() - stopAlarmAndFinish() - } - - override fun onUserLeaveHint() { - super.onUserLeaveHint() + override fun onDestroy() { stopAlarmAndFinish() + super.onDestroy() } private fun showWhenLockedAndTurnScreenOn() { @@ -116,6 +121,30 @@ class LocationAlarmActivity : ComponentActivity() { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } + private fun startAlarmRingtone() { + try { + val alarmUri: Uri? = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM) + ?: RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) + ?: RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + + if (alarmUri != null) { + ringtone = RingtoneManager.getRingtone(applicationContext, alarmUri)?.apply { + val attributes = AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_ALARM) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build() + audioAttributes = attributes + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + isLooping = true + } + play() + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + private fun startUrgentVibration() { val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val vibratorManager = @@ -137,6 +166,17 @@ class LocationAlarmActivity : ComponentActivity() { } private fun stopAlarmAndFinish() { + try { + ringtone?.let { + if (it.isPlaying) { + it.stop() + } + } + ringtone = null + } catch (e: Exception) { + e.printStackTrace() + } + val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val vibratorManager = getSystemService(VIBRATOR_MANAGER_SERVICE) as VibratorManager @@ -151,6 +191,15 @@ class LocationAlarmActivity : ComponentActivity() { e.printStackTrace() } + // Cancel all notifications + try { + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(1001) // ALARM_NOTIFICATION_ID + notificationManager.cancel(2001) // NOTIFICATION_ID + } catch (e: Exception) { + e.printStackTrace() + } + // Disable alarm in repo val repo = LocationReachedRepository(this) val activeId = repo.getActiveAlarmId() @@ -235,8 +284,12 @@ fun LocationAlarmScreen(onFinish: () -> Unit) { Spacer(modifier = Modifier.height(80.dp)) + val view = LocalView.current Button( - onClick = onFinish, + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + onFinish() + }, modifier = Modifier .fillMaxWidth(0.7f) .height(64.dp), diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/location/LocationReachedSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/location/LocationReachedSettingsUI.kt index 651cdfd01..d244b0d99 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/location/LocationReachedSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/location/LocationReachedSettingsUI.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.sameerasw.essentials.R +import com.sameerasw.essentials.ui.core.cards.IconToggleItem import com.sameerasw.essentials.ui.core.cards.LocationAlarmCard import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer import com.sameerasw.essentials.ui.core.sheets.LocationReachedBottomSheet @@ -208,6 +209,25 @@ fun LocationReachedSettingsUI( ) } + // Settings Card + item { + val isFullScreenAlarmEnabled by mainViewModel.isLocationReachedFullScreenAlarmEnabled + RoundedCardContainer( + modifier = Modifier.fillMaxWidth() + ) { + IconToggleItem( + title = stringResource(R.string.setting_location_reached_full_screen_alarm_title), + description = stringResource(R.string.setting_location_reached_full_screen_alarm_desc), + isChecked = isFullScreenAlarmEnabled, + onCheckedChange = { enabled -> + HapticUtil.performUIHaptic(view) + mainViewModel.setLocationReachedFullScreenAlarmEnabled(enabled) + }, + iconRes = R.drawable.rounded_alarm_24 + ) + } + } + // Permission Warning item { val isFSIGranted by mainViewModel.isFullScreenIntentPermissionGranted @@ -216,7 +236,7 @@ fun LocationReachedSettingsUI( modifier = Modifier.fillMaxWidth(), containerColor = MaterialTheme.colorScheme.errorContainer ) { - com.sameerasw.essentials.ui.core.cards.IconToggleItem( + IconToggleItem( title = stringResource(R.string.location_reached_fsi_title), description = stringResource(R.string.location_reached_fsi_desc), isChecked = false, 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 e0945b3d3..5f21eb69f 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -287,6 +287,7 @@ class MainViewModel : ViewModel() { val isPitchBlackThemeEnabled = mutableStateOf(false) val isGenAIAutomationEnabled = mutableStateOf(false) + val isLocationReachedFullScreenAlarmEnabled = mutableStateOf(true) val isEnableUnsupportedFeatures = mutableStateOf(false) val isBlurEnabled = mutableStateOf(true) @@ -548,6 +549,10 @@ class MainViewModel : ViewModel() { SettingsRepository.KEY_PITCH_BLACK_THEME_ENABLED -> isPitchBlackThemeEnabled.value = settingsRepository.getBoolean(key) + SettingsRepository.KEY_LOCATION_REACHED_FULL_SCREEN_ALARM_ENABLED -> + isLocationReachedFullScreenAlarmEnabled.value = + settingsRepository.getLocationReachedFullScreenAlarmEnabled() + SettingsRepository.KEY_ENABLE_UNSUPPORTED_FEATURES -> { isEnableUnsupportedFeatures.value = settingsRepository.isEnableUnsupportedFeatures() @@ -1597,6 +1602,8 @@ class MainViewModel : ViewModel() { ) isPitchBlackThemeEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_PITCH_BLACK_THEME_ENABLED) + isLocationReachedFullScreenAlarmEnabled.value = + settingsRepository.getLocationReachedFullScreenAlarmEnabled() isEnableUnsupportedFeatures.value = settingsRepository.isEnableUnsupportedFeatures() keyboardHeight.floatValue = @@ -2234,6 +2241,11 @@ class MainViewModel : ViewModel() { settingsRepository.putBoolean(SettingsRepository.KEY_PITCH_BLACK_THEME_ENABLED, enabled) } + fun setLocationReachedFullScreenAlarmEnabled(enabled: Boolean) { + isLocationReachedFullScreenAlarmEnabled.value = enabled + settingsRepository.setLocationReachedFullScreenAlarmEnabled(enabled) + } + /** * Executes the set blur enabled operation. * diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3ad5be49e..2dd1ff885 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1465,6 +1465,8 @@ Prepare to get off Dismiss Destination set: %1$.4f, %2$.4f + Full-screen alarm + Play alarm ringtone and show full-screen alert upon arrival Use Root Instead of Shizuku Root access not available. Please check your root manager.