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
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" />
+
+
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"
// 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"
@@ -122,6 +178,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"
@@ -228,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"
@@ -2821,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/domain/diy/Action.kt b/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt
index 4125db242..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
@@ -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,12 +165,72 @@ 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
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/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..24694b8bd
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/domain/diy/ActionRegistry.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.SetVolume(),
+ 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/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/services/automation/executors/CombinedActionExecutor.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt
index 3ac2e50d9..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
@@ -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,55 @@ 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()
+ }
+
+ 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/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/AutomationEditorActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt
index 417f534a0..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
@@ -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
@@ -103,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
@@ -270,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) {
@@ -884,67 +887,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 {
@@ -1015,6 +958,7 @@ class AutomationEditorActivity : ComponentActivity() {
is Action.Keyboard -> {
showSetKeyboardSheet = true
}
+ is Action.SetVolume -> showSetVolumeSettings = true
is Action.CustomSettings -> showCustomSettingsSettings = true
else -> {}
}
@@ -1202,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/activities/DebuggingSettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/DebuggingSettingsActivity.kt
new file mode 100644
index 000000000..681bd472d
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/DebuggingSettingsActivity.kt
@@ -0,0 +1,287 @@
+/*
+ * Copyright (c) 2026 sameerasw.com
+ * License: MIT License
+ *
+ * Feature Module: Application Activities
+ * File: DebuggingSettingsActivity.kt
+ * Description: Bottom sheet dialog activity for toggling USB and Wireless Debugging, launched on long-pressing the Debugging QS Tile.
+ */
+
+package com.sameerasw.essentials.ui.activities
+
+import android.content.Context
+import android.content.Intent
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.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
+
+class DebuggingSettingsActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ val viewModel: MainViewModel = viewModel()
+ val context = LocalContext.current
+ LaunchedEffect(Unit) {
+ viewModel.check(context)
+ }
+ val isPitchBlackThemeEnabled by viewModel.isPitchBlackThemeEnabled
+ EssentialsTheme(pitchBlackTheme = isPitchBlackThemeEnabled) {
+ DebuggingSettingsOverlay(onDismiss = { finish() })
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DebuggingSettingsOverlay(onDismiss: () -> 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)
+ }
+ )
+ }
+
+ 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)
+ 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/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/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/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/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/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/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 92f7c59fb..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
@@ -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,24 @@ 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.features.audio.sheets.SetVolumeSettingsSheet
import com.sameerasw.essentials.ui.modifiers.highlight
import com.sameerasw.essentials.utils.HapticUtil
import com.sameerasw.essentials.viewmodels.MainViewModel
@@ -68,20 +80,31 @@ 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 showSetVolumeSettings 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 +123,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 +200,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 +233,6 @@ fun ButtonRemapSettingsUI(
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut()
) {
- // Status indicator
Row(
modifier = Modifier
.fillMaxWidth()
@@ -250,8 +307,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 +334,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 +362,104 @@ 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.SetVolume -> showSetVolumeSettings = true
+ is Action.CustomSettings -> showCustomSettingsSettings = true
+ else -> {}
+ }
+ }
+ )
+ }
}
}
}
@@ -438,7 +478,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 +520,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 +541,169 @@ 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 (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,
+ 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/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/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..5f21eb69f 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)
@@ -286,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)
@@ -547,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()
@@ -1523,30 +1529,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,
@@ -1616,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 =
@@ -2253,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.
*
@@ -2889,6 +2882,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.
*
@@ -3535,23 +3559,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 +3584,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/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 29f120a19..2dd1ff885 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -304,6 +304,14 @@
Toggle
Cycle
Debugging
+ USB Debugging
+ 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
@@ -993,6 +1001,17 @@
AI Assistant
Take Screenshot
Toggle Media Volume
+ Set Volume
+ Volume Level
+ Music
+ Ring
+ Alarm
+ Call
+ Notification
+ System
+ Cycle Sound Modes
+ Toggle Mute
+ Toggle Vibrate
Like Current Song
Circle to Search
Pin Foreground App
@@ -1446,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.
@@ -1971,6 +1992,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.