Skip to content
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ build/

local.properties
.agents/
*build.gradle.kts.bak
8 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,14 @@
android:taskAffinity=""
android:theme="@style/Theme.Essentials.Translucent" />

<activity
android:name=".ui.activities.DebuggingSettingsActivity"
android:excludeFromRecents="true"
android:exported="true"
android:noHistory="true"
android:taskAffinity=""
android:theme="@style/Theme.Essentials.Translucent" />

<activity
android:name=".ui.activities.BatteryDetailsActivity"
android:excludeFromRecents="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import android.content.SharedPreferences
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.sameerasw.essentials.domain.HapticFeedbackType
import com.sameerasw.essentials.domain.diy.Action
import com.sameerasw.essentials.domain.diy.ActionGsonAdapter
import com.sameerasw.essentials.domain.model.AppSelection
import com.sameerasw.essentials.domain.model.AppTag
import com.sameerasw.essentials.domain.model.DnsPreset
Expand All @@ -38,6 +40,7 @@ class SettingsRepository(private val context: Context) {

init {
migrateUsageAccessKey()
migrateRemapStringToAction()
}

private fun migrateUsageAccessKey() {
Expand All @@ -51,10 +54,63 @@ class SettingsRepository(private val context: Context) {
}
}

private fun migrateRemapStringToAction() {
if (getBoolean(KEY_BUTTON_REMAP_MIGRATION_DONE)) return

val remapKeys = listOf(
KEY_BUTTON_REMAP_VOL_UP_ACTION_OFF,
KEY_BUTTON_REMAP_VOL_DOWN_ACTION_OFF,
KEY_BUTTON_REMAP_VOL_UP_ACTION_ON,
KEY_BUTTON_REMAP_VOL_DOWN_ACTION_ON
)
for (key in remapKeys) {
val raw = prefs.getString(key, null) ?: continue
// Skip if already JSON (starts with '{') — already migrated or set by new code
if (raw.startsWith("{")) continue
val action: Action? = when (raw) {
"Toggle flashlight" -> Action.ToggleFlashlight
"Media play/pause" -> Action.MediaPlayPause
"Media next" -> Action.MediaNext
"Media previous" -> Action.MediaPrevious
"Toggle vibrate" -> Action.ToggleVibrate
"Toggle mute" -> Action.ToggleMute
"AI assistant" -> Action.AIAssistant
"Take screenshot" -> Action.TakeScreenshot
"Cycle sound modes" -> Action.CycleSoundModes
"Toggle media volume" -> Action.ToggleMediaVolume
"Like current song" -> Action.LikeCurrentSong
"Circle to Search" -> Action.CircleToSearch
else -> null
}
setRemapAction(key, action)
}

putBoolean(KEY_BUTTON_REMAP_MIGRATION_DONE, true)
}

fun getRemapAction(key: String): Action? {
val json = prefs.getString(key, null) ?: return null
return try {
ActionGsonAdapter.fromJson(json)
} catch (_: Exception) {
null
}
}

fun setRemapAction(key: String, action: Action?) {
if (action == null) {
prefs.edit().remove(key).apply()
} else {
prefs.edit().putString(key, ActionGsonAdapter.toJson(action)).apply()
}
}


companion object {
const val PREFS_NAME = "essentials_prefs"

// 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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}

72 changes: 60 additions & 12 deletions app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String> = 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<String> = 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<String> = 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<T : Any>(private val kClass: KClass<T>) :
JsonSerializer<T>, JsonDeserializer<T> {

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
}
}
Original file line number Diff line number Diff line change
@@ -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<Action>
)

/**
* 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<ActionCategory> {
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)
)
}
}
Loading
Loading