diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 0a150fc6..8cf076a9 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -70,6 +70,7 @@ dependencies {
implementation(libs.androidx.ui)
implementation(libs.androidx.compose.foundation)
implementation(libs.material)
+ implementation(libs.androidx.material3)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(platform(libs.androidx.compose.bom))
@@ -97,4 +98,7 @@ dependencies {
// Google Maps & Location
implementation(libs.play.services.location)
+
+ // Kotlin Reflect for dynamic sealed class serialization
+ implementation(kotlin("reflect"))
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9e9328b1..8d2c9a66 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -87,6 +87,14 @@
android:launchMode="singleInstance"
android:theme="@style/Theme.Essentials.FullScreenAlarm" />
+
+
+
+
@@ -370,6 +378,15 @@
android:exported="false"
android:foregroundServiceType="specialUse" />
+
+
+
+
= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(screenOffReceiver, intentFilter, Context.RECEIVER_EXPORTED)
diff --git a/app/src/main/java/com/sameerasw/essentials/MainActivity.kt b/app/src/main/java/com/sameerasw/essentials/MainActivity.kt
index 6a993e2f..bee2426f 100644
--- a/app/src/main/java/com/sameerasw/essentials/MainActivity.kt
+++ b/app/src/main/java/com/sameerasw/essentials/MainActivity.kt
@@ -39,7 +39,7 @@ import com.sameerasw.essentials.domain.registry.initPermissionRegistry
import com.sameerasw.essentials.ui.components.ReusableTopAppBar
import com.sameerasw.essentials.ui.components.DIYFloatingToolbar
import com.sameerasw.essentials.ui.composables.SetupFeatures
-import com.sameerasw.essentials.ui.composables.ComingSoonDIYScreen
+import com.sameerasw.essentials.ui.composables.DIYScreen
import com.sameerasw.essentials.ui.theme.EssentialsTheme
import com.sameerasw.essentials.utils.HapticUtil
import com.sameerasw.essentials.viewmodels.MainViewModel
@@ -279,7 +279,7 @@ class MainActivity : FragmentActivity() {
)
}
DIYTabs.DIY -> {
- ComingSoonDIYScreen(
+ DIYScreen(
modifier = Modifier.padding(innerPadding)
)
}
diff --git a/app/src/main/java/com/sameerasw/essentials/ShortcutHandlerActivity.kt b/app/src/main/java/com/sameerasw/essentials/ShortcutHandlerActivity.kt
index 6c16aef9..e02c9e81 100644
--- a/app/src/main/java/com/sameerasw/essentials/ShortcutHandlerActivity.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ShortcutHandlerActivity.kt
@@ -13,6 +13,7 @@ import androidx.compose.material3.LoadingIndicator
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
+import com.sameerasw.essentials.ui.activities.AppFreezingActivity
import com.sameerasw.essentials.ui.theme.EssentialsTheme
import com.sameerasw.essentials.utils.FreezeManager
import kotlinx.coroutines.CoroutineScope
diff --git a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt
index 3284cdf3..0ad53aae 100644
--- a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt
+++ b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt
@@ -279,7 +279,7 @@ class SettingsRepository(private val context: Context) {
fun getAllConfigsAsJsonString(): String {
return try {
val allConfigs = mutableMapOf>>()
- val prefFiles = listOf("essentials_prefs", "caffeinate_prefs", "link_prefs")
+ val prefFiles = listOf("essentials_prefs", "caffeinate_prefs", "link_prefs", "diy_automations_prefs")
prefFiles.forEach { fileName ->
val p = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
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
new file mode 100644
index 00000000..ffeea6f4
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt
@@ -0,0 +1,53 @@
+package com.sameerasw.essentials.domain.diy
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import com.sameerasw.essentials.R
+
+sealed interface Action {
+ @get:StringRes
+ val title: Int
+ @get:DrawableRes
+ val icon: Int
+ val permissions: List
+ get() = emptyList()
+ val isConfigurable: Boolean
+ get() = false
+
+ data object HapticVibration : Action {
+ override val title: Int = R.string.diy_action_haptic
+ override val icon: Int = R.drawable.rounded_mobile_vibrate_24
+ }
+
+ data object ShowNotification : Action {
+ override val title: Int = R.string.diy_action_notification
+ override val icon: Int = R.drawable.rounded_notifications_unread_24
+ }
+
+ data object RemoveNotification : Action {
+ override val title: Int = R.string.diy_action_remove_notification
+ override val icon: Int = R.drawable.rounded_notifications_off_24
+ }
+
+ data object TurnOnFlashlight : Action {
+ override val title: Int = R.string.diy_action_flashlight_on
+ override val icon: Int = R.drawable.round_flashlight_on_24
+ }
+
+ data object TurnOffFlashlight : Action {
+ override val title: Int = R.string.diy_action_flashlight_off
+ override val icon: Int = R.drawable.rounded_flashlight_on_24
+ }
+
+ data object ToggleFlashlight : Action {
+ override val title: Int = R.string.diy_action_flashlight_toggle
+ override val icon: Int = R.drawable.rounded_flashlight_on_24
+ }
+
+ data class DimWallpaper(val dimAmount: Float = 0f) : Action {
+ override val title: Int = R.string.diy_action_dim_wallpaper
+ override val icon: Int = R.drawable.rounded_mobile_screensaver_24
+ override val permissions: List = listOf("shizuku", "root")
+ override val isConfigurable: Boolean = true
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/domain/diy/Automation.kt b/app/src/main/java/com/sameerasw/essentials/domain/diy/Automation.kt
new file mode 100644
index 00000000..94e3c71f
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/domain/diy/Automation.kt
@@ -0,0 +1,17 @@
+package com.sameerasw.essentials.domain.diy
+
+data class Automation(
+ val id: String,
+ val type: Type,
+ val trigger: Trigger? = null,
+ val state: State? = null,
+ val actions: List = emptyList(),
+ val entryAction: Action? = null,
+ val exitAction: Action? = null,
+ val isEnabled: Boolean = true
+) {
+ enum class Type {
+ TRIGGER,
+ STATE
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/domain/diy/DIYRepository.kt b/app/src/main/java/com/sameerasw/essentials/domain/diy/DIYRepository.kt
new file mode 100644
index 00000000..b12d89c6
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/domain/diy/DIYRepository.kt
@@ -0,0 +1,114 @@
+package com.sameerasw.essentials.domain.diy
+
+import android.content.Context
+import android.content.SharedPreferences
+import com.google.gson.GsonBuilder
+import com.google.gson.JsonDeserializationContext
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+import com.google.gson.JsonSerializationContext
+import com.google.gson.JsonSerializer
+import com.google.gson.reflect.TypeToken
+import com.sameerasw.essentials.domain.diy.Action
+import kotlin.reflect.KClass
+import com.sameerasw.essentials.domain.diy.Automation
+import com.sameerasw.essentials.domain.diy.State
+import com.sameerasw.essentials.domain.diy.Trigger
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+object DIYRepository {
+ private const val PREFS_NAME = "diy_automations_prefs"
+ private const val KEY_AUTOMATIONS = "saved_automations"
+
+ private val _automations = MutableStateFlow>(emptyList())
+ val automations = _automations.asStateFlow()
+
+ private var prefs: SharedPreferences? = null
+ private val gson = GsonBuilder()
+ .registerTypeAdapter(Trigger::class.java, SealedAdapter(Trigger::class))
+ .registerTypeAdapter(State::class.java, SealedAdapter(State::class))
+ .registerTypeAdapter(Action::class.java, SealedAdapter(Action::class))
+ .create()
+
+ 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
+ val subClass = kClass.sealedSubclasses.firstOrNull { it.simpleName == typeName }
+
+ return if (subClass != null) {
+ // Determine if it's an object or class
+ if (subClass.objectInstance != null) {
+ subClass.objectInstance
+ } else {
+ context.deserialize(json, subClass.java)
+ }
+ } else {
+ null
+ }
+ }
+ }
+
+ fun init(context: Context) {
+ if (prefs != null) return
+ prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+ loadAutomations()
+ }
+
+ private fun loadAutomations() {
+ val json = prefs?.getString(KEY_AUTOMATIONS, null)
+ val loadedList: List = if (json != null) {
+ try {
+ val type = object : TypeToken>() {}.type
+ gson.fromJson(json, type) ?: emptyList()
+ } catch (e: Exception) {
+ emptyList()
+ }
+ } else {
+ emptyList()
+ }
+ _automations.value = loadedList
+ }
+
+ private fun saveToPrefs() {
+ val json = gson.toJson(_automations.value)
+ prefs?.edit()?.putString(KEY_AUTOMATIONS, json)?.apply()
+ }
+
+ fun addAutomation(automation: Automation) {
+ val current = _automations.value.toMutableList()
+ current.add(automation)
+ _automations.value = current
+ saveToPrefs()
+ }
+
+ fun updateAutomation(automation: Automation) {
+ val current = _automations.value.toMutableList()
+ val index = current.indexOfFirst { it.id == automation.id }
+ if (index != -1) {
+ current[index] = automation
+ _automations.value = current
+ saveToPrefs()
+ }
+ }
+
+ fun removeAutomation(id: String) {
+ val current = _automations.value.toMutableList()
+ current.removeAll { it.id == id }
+ _automations.value = current
+ saveToPrefs()
+ }
+
+ fun getAutomation(id: String): Automation? {
+ return _automations.value.find { it.id == id }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/domain/diy/State.kt b/app/src/main/java/com/sameerasw/essentials/domain/diy/State.kt
new file mode 100644
index 00000000..9b17c51d
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/domain/diy/State.kt
@@ -0,0 +1,22 @@
+package com.sameerasw.essentials.domain.diy
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import com.sameerasw.essentials.R
+
+sealed interface State {
+ @get:StringRes
+ val title: Int
+ @get:DrawableRes
+ val icon: Int
+
+ data object Charging : State {
+ override val title: Int = R.string.diy_state_charging
+ override val icon: Int = R.drawable.rounded_charger_24
+ }
+
+ data object ScreenOn : State {
+ override val title: Int = R.string.diy_state_screen_on
+ override val icon: Int = R.drawable.rounded_mobile_text_2_24
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/domain/diy/Trigger.kt b/app/src/main/java/com/sameerasw/essentials/domain/diy/Trigger.kt
new file mode 100644
index 00000000..a46a252d
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/domain/diy/Trigger.kt
@@ -0,0 +1,38 @@
+package com.sameerasw.essentials.domain.diy
+
+import androidx.annotation.StringRes
+import com.sameerasw.essentials.R
+
+sealed interface Trigger {
+ val title: Int
+ val icon: Int
+ val permissions: List
+ get() = emptyList()
+ val isConfigurable: Boolean
+ get() = false
+
+ data object ScreenOff : Trigger {
+ override val title: Int = R.string.diy_trigger_screen_off
+ override val icon: Int = R.drawable.rounded_mobile_lock_portrait_24
+ }
+
+ data object ScreenOn : Trigger {
+ override val title: Int = R.string.diy_trigger_screen_on
+ override val icon: Int = R.drawable.rounded_mobile_text_2_24
+ }
+
+ data object DeviceUnlock : Trigger {
+ override val title: Int = R.string.diy_trigger_device_unlock
+ override val icon: Int = R.drawable.rounded_mobile_unlock_24
+ }
+
+ data object ChargerConnected : Trigger {
+ override val title: Int = R.string.diy_trigger_charger_connected
+ override val icon: Int = R.drawable.rounded_battery_charging_60_24
+ }
+
+ data object ChargerDisconnected : Trigger {
+ override val title: Int = R.string.diy_trigger_charger_disconnected
+ override val icon: Int = R.drawable.rounded_battery_android_frame_3_24
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationManager.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationManager.kt
new file mode 100644
index 00000000..6ad80e23
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationManager.kt
@@ -0,0 +1,144 @@
+package com.sameerasw.essentials.services.automation
+
+import android.content.Context
+import android.content.Intent
+import com.sameerasw.essentials.domain.diy.Automation
+import com.sameerasw.essentials.domain.diy.DIYRepository
+import com.sameerasw.essentials.domain.diy.Trigger
+import com.sameerasw.essentials.domain.diy.State as DIYState
+import com.sameerasw.essentials.services.automation.modules.AutomationModule
+import com.sameerasw.essentials.services.automation.modules.DisplayModule
+import com.sameerasw.essentials.services.automation.modules.PowerModule
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import java.util.concurrent.ConcurrentHashMap
+
+object AutomationManager {
+ private val scope = CoroutineScope(Dispatchers.Main)
+ private var service: AutomationService? = null
+ private var applicationContext: Context? = null
+
+ // Active modules map: ModuleID -> Module Instance
+ private val activeModules = ConcurrentHashMap()
+
+ fun init(context: Context) {
+ applicationContext = context.applicationContext
+
+ // Observe repository
+ scope.launch {
+ DIYRepository.automations.collect { automations ->
+ refreshModules(automations)
+ }
+ }
+ }
+
+ fun onServiceConnected(serviceInstance: AutomationService) {
+ service = serviceInstance
+ // Re-apply modules if needed
+ val automations = DIYRepository.automations.value
+ refreshModules(automations)
+ }
+
+ fun onServiceDisconnected(serviceInstance: AutomationService) {
+ if (service == serviceInstance) {
+ service = null
+ stopAllModules()
+ }
+ }
+
+ private fun stopAllModules() {
+ val context = applicationContext ?: return
+ activeModules.values.forEach { it.stop(context) }
+ activeModules.clear()
+ }
+
+ private fun refreshModules(automations: List) {
+ val context = applicationContext ?: return
+ val enabledAutomations = automations.filter { it.isEnabled }
+
+ // Determine required modules
+ val requiredModuleIds = mutableSetOf()
+ val powerAutomations = mutableListOf()
+ val displayAutomations = mutableListOf()
+
+ enabledAutomations.forEach { automation ->
+ when (automation.type) {
+ Automation.Type.TRIGGER -> {
+ when (automation.trigger) {
+ is Trigger.ChargerConnected, is Trigger.ChargerDisconnected -> {
+ requiredModuleIds.add(PowerModule.ID)
+ powerAutomations.add(automation)
+ }
+ is Trigger.ScreenOn, is Trigger.ScreenOff, is Trigger.DeviceUnlock -> {
+ requiredModuleIds.add(DisplayModule.ID)
+ displayAutomations.add(automation)
+ }
+ else -> {}
+ }
+ }
+ Automation.Type.STATE -> {
+ when (automation.state) {
+ is DIYState.Charging -> {
+ requiredModuleIds.add(PowerModule.ID)
+ powerAutomations.add(automation)
+ }
+ is DIYState.ScreenOn -> {
+ requiredModuleIds.add(DisplayModule.ID)
+ displayAutomations.add(automation)
+ }
+ else -> {}
+ }
+ }
+ }
+ }
+
+ // Service Lifecycle Management
+ if (requiredModuleIds.isNotEmpty()) {
+ startService(context)
+ } else {
+ stopService(context)
+ stopAllModules()
+ return
+ }
+
+ // Module Management
+
+ // Power Module
+ if (requiredModuleIds.contains(PowerModule.ID)) {
+ val module = activeModules.getOrPut(PowerModule.ID) {
+ PowerModule().also { it.start(context) }
+ }
+ module.updateAutomations(powerAutomations)
+ } else {
+ activeModules.remove(PowerModule.ID)?.stop(context)
+ }
+
+ // Display Module
+ if (requiredModuleIds.contains(DisplayModule.ID)) {
+ val module = activeModules.getOrPut(DisplayModule.ID) {
+ DisplayModule().also { it.start(context) }
+ }
+ module.updateAutomations(displayAutomations)
+ } else {
+ activeModules.remove(DisplayModule.ID)?.stop(context)
+ }
+ }
+
+ private fun startService(context: Context) {
+ if (!AutomationService.isRunning) {
+ val intent = Intent(context, AutomationService::class.java)
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ context.startForegroundService(intent)
+ } else {
+ context.startService(intent)
+ }
+ }
+ }
+
+ private fun stopService(context: Context) {
+ if (AutomationService.isRunning) {
+ context.stopService(Intent(context, AutomationService::class.java))
+ }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationService.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationService.kt
new file mode 100644
index 00000000..1c6e5c8f
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationService.kt
@@ -0,0 +1,63 @@
+package com.sameerasw.essentials.services.automation
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Intent
+import android.content.pm.ServiceInfo
+import android.os.Build
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+import com.sameerasw.essentials.R
+import com.sameerasw.essentials.services.automation.modules.AutomationModule
+
+class AutomationService : Service() {
+
+ companion object {
+ private const val CHANNEL_ID = "automation_service_channel"
+ private const val NOTIFICATION_ID = 999
+ var isRunning = false
+ }
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ override fun onCreate() {
+ super.onCreate()
+ isRunning = true
+ createNotificationChannel()
+ startForeground(NOTIFICATION_ID, createNotification(),
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE else 0
+ )
+
+ // Modules will be started by AutomationManager calling onServiceCreated/Updated
+ AutomationManager.onServiceConnected(this)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ isRunning = false
+ AutomationManager.onServiceDisconnected(this)
+ }
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ getString(R.string.automation_service_channel_name),
+ NotificationManager.IMPORTANCE_LOW
+ )
+ val manager = getSystemService(NotificationManager::class.java)
+ manager.createNotificationChannel(channel)
+ }
+ }
+
+ private fun createNotification(): Notification {
+ return NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle(getString(R.string.automation_service_running_title))
+ .setContentText(getString(R.string.automation_service_running_desc))
+ .setSmallIcon(R.drawable.outline_bubble_chart_24)
+ .setOngoing(true)
+ .build()
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/automation/executors/ActionExecutor.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/executors/ActionExecutor.kt
new file mode 100644
index 00000000..a257bace
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/services/automation/executors/ActionExecutor.kt
@@ -0,0 +1,8 @@
+package com.sameerasw.essentials.services.automation.executors
+
+import android.content.Context
+import com.sameerasw.essentials.domain.diy.Action
+
+interface ActionExecutor {
+ suspend fun execute(context: Context, action: Action)
+}
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
new file mode 100644
index 00000000..3f3c496b
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt
@@ -0,0 +1,75 @@
+package com.sameerasw.essentials.services.automation.executors
+
+import android.content.Context
+import android.content.Intent
+import android.hardware.camera2.CameraManager
+import android.os.Build
+import com.sameerasw.essentials.domain.diy.Action
+import com.sameerasw.essentials.utils.HapticUtil
+import android.view.View
+
+object CombinedActionExecutor {
+
+ suspend fun execute(context: Context, action: Action) {
+ when (action) {
+ is Action.HapticVibration -> {
+ val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ val manager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as android.os.VibratorManager
+ manager.defaultVibrator
+ } else {
+ context.getSystemService(Context.VIBRATOR_SERVICE) as android.os.Vibrator
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ vibrator.vibrate(android.os.VibrationEffect.createOneShot(50, android.os.VibrationEffect.DEFAULT_AMPLITUDE))
+ } else {
+ @Suppress("DEPRECATION")
+ vibrator.vibrate(50)
+ }
+ }
+
+ is Action.TurnOnFlashlight -> toggleFlashlight(context, true)
+ is Action.TurnOffFlashlight -> toggleFlashlight(context, false)
+ is Action.ToggleFlashlight -> {
+ val camManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+ try {
+ val cameraId = camManager.cameraIdList[0]
+ camManager.registerTorchCallback(object : CameraManager.TorchCallback() {
+ override fun onTorchModeChanged(cameraId: String, enabled: Boolean) {
+ super.onTorchModeChanged(cameraId, enabled)
+ camManager.unregisterTorchCallback(this)
+ try {
+ camManager.setTorchMode(cameraId, !enabled)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }, null)
+
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ is Action.ShowNotification -> {
+ // Placeholder
+ }
+ is Action.RemoveNotification -> {
+ // Placeholder
+ }
+ is Action.DimWallpaper -> {
+ com.sameerasw.essentials.utils.ShellUtils.runCommand(context, "cmd wallpaper set-dim-amount ${action.dimAmount}")
+ }
+ }
+ }
+
+ private fun toggleFlashlight(context: Context, on: Boolean) {
+ val camManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+ try {
+ val cameraId = camManager.cameraIdList[0]
+ camManager.setTorchMode(cameraId, on)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/automation/modules/AutomationModule.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/modules/AutomationModule.kt
new file mode 100644
index 00000000..1a276827
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/services/automation/modules/AutomationModule.kt
@@ -0,0 +1,11 @@
+package com.sameerasw.essentials.services.automation.modules
+
+import android.content.Context
+import com.sameerasw.essentials.domain.diy.Automation
+
+interface AutomationModule {
+ val id: String
+ fun start(context: Context)
+ fun stop(context: Context)
+ fun updateAutomations(automations: List)
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/automation/modules/DisplayModule.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/modules/DisplayModule.kt
new file mode 100644
index 00000000..300db1e0
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/services/automation/modules/DisplayModule.kt
@@ -0,0 +1,90 @@
+package com.sameerasw.essentials.services.automation.modules
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import com.sameerasw.essentials.domain.diy.Automation
+import com.sameerasw.essentials.domain.diy.Trigger
+import com.sameerasw.essentials.services.automation.executors.CombinedActionExecutor
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import com.sameerasw.essentials.domain.diy.State as DIYState
+
+class DisplayModule : AutomationModule {
+ companion object {
+ const val ID = "display_module"
+ }
+
+ override val id: String = ID
+ private var automations: List = emptyList()
+ private val scope = CoroutineScope(Dispatchers.IO)
+
+ private val receiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ when (intent.action) {
+ Intent.ACTION_SCREEN_ON -> {
+ handleTrigger(context, Trigger.ScreenOn)
+ handleStateChange(context, true)
+ }
+ Intent.ACTION_SCREEN_OFF -> {
+ handleTrigger(context, Trigger.ScreenOff)
+ handleStateChange(context, false)
+ }
+ Intent.ACTION_USER_PRESENT -> {
+ handleTrigger(context, Trigger.DeviceUnlock)
+ }
+ }
+ }
+ }
+
+ override fun start(context: Context) {
+ val filter = IntentFilter().apply {
+ addAction(Intent.ACTION_SCREEN_ON)
+ addAction(Intent.ACTION_SCREEN_OFF)
+ addAction(Intent.ACTION_USER_PRESENT)
+ }
+ context.registerReceiver(receiver, filter)
+ }
+
+ override fun stop(context: Context) {
+ try {
+ context.unregisterReceiver(receiver)
+ } catch (e: Exception) {
+ // Ignore
+ }
+ }
+
+ override fun updateAutomations(automations: List) {
+ this.automations = automations
+ }
+
+ private fun handleTrigger(context: Context, trigger: Trigger) {
+ scope.launch {
+ automations.filter { it.type == Automation.Type.TRIGGER && it.trigger == trigger }
+ .forEach { automation ->
+ automation.actions.forEach { action ->
+ CombinedActionExecutor.execute(context, action)
+ }
+ }
+ }
+ }
+
+ private fun handleStateChange(context: Context, isActive: Boolean) {
+ scope.launch {
+ automations.filter { it.type == Automation.Type.STATE }
+ .forEach { automation ->
+ if (automation.state is DIYState.ScreenOn) {
+ if (isActive) {
+ // Entry
+ automation.entryAction?.let { CombinedActionExecutor.execute(context, it) }
+ } else {
+ // Exit
+ automation.exitAction?.let { CombinedActionExecutor.execute(context, it) }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/automation/modules/PowerModule.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/modules/PowerModule.kt
new file mode 100644
index 00000000..e0d0200c
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/services/automation/modules/PowerModule.kt
@@ -0,0 +1,108 @@
+package com.sameerasw.essentials.services.automation.modules
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.BatteryManager
+import com.sameerasw.essentials.domain.diy.Automation
+import com.sameerasw.essentials.domain.diy.Trigger
+import com.sameerasw.essentials.services.automation.executors.CombinedActionExecutor
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import com.sameerasw.essentials.domain.diy.State as DIYState
+
+class PowerModule : AutomationModule {
+ companion object {
+ const val ID = "power_module"
+ }
+
+ override val id: String = ID
+ private var automations: List = emptyList()
+ private val scope = CoroutineScope(Dispatchers.IO)
+
+ // State tracking
+ private var isCharging = false
+
+ private val receiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ when (intent.action) {
+ Intent.ACTION_POWER_CONNECTED -> {
+ if (!isCharging) {
+ isCharging = true
+ handleTrigger(context, Trigger.ChargerConnected)
+ handleStateChange(context, true)
+ }
+ }
+ Intent.ACTION_POWER_DISCONNECTED -> {
+ if (isCharging) {
+ isCharging = false
+ handleTrigger(context, Trigger.ChargerDisconnected)
+ handleStateChange(context, false)
+ }
+ }
+ }
+ }
+ }
+
+ override fun start(context: Context) {
+ val filter = IntentFilter().apply {
+ addAction(Intent.ACTION_POWER_CONNECTED)
+ addAction(Intent.ACTION_POWER_DISCONNECTED)
+ }
+ context.registerReceiver(receiver, filter)
+
+ // Initial check
+ val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { ifilter ->
+ context.registerReceiver(null, ifilter)
+ }
+ val status: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
+ isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
+ status == BatteryManager.BATTERY_STATUS_FULL
+
+ if (isCharging) {
+ handleStateChange(context, true)
+ }
+ }
+
+ override fun stop(context: Context) {
+ try {
+ context.unregisterReceiver(receiver)
+ } catch (e: Exception) {
+ // Ignore if not registered
+ }
+ }
+
+ override fun updateAutomations(automations: List) {
+ this.automations = automations
+ }
+
+ private fun handleTrigger(context: Context, trigger: Trigger) {
+ scope.launch {
+ automations.filter { it.type == Automation.Type.TRIGGER && it.trigger == trigger }
+ .forEach { automation ->
+ automation.actions.forEach { action ->
+ CombinedActionExecutor.execute(context, action)
+ }
+ }
+ }
+ }
+
+ private fun handleStateChange(context: Context, isActive: Boolean) {
+ scope.launch {
+ automations.filter { it.type == Automation.Type.STATE }
+ .forEach { automation ->
+ if (automation.state is DIYState.Charging) {
+ if (isActive) {
+ // Entry
+ automation.entryAction?.let { CombinedActionExecutor.execute(context, it) }
+ } else {
+ // Exit
+ automation.exitAction?.let { CombinedActionExecutor.execute(context, it) }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/AppFreezingTileService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/AppFreezingTileService.kt
index 651508b0..ea741c17 100644
--- a/app/src/main/java/com/sameerasw/essentials/services/tiles/AppFreezingTileService.kt
+++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/AppFreezingTileService.kt
@@ -4,7 +4,7 @@ import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import android.service.quicksettings.Tile
-import com.sameerasw.essentials.AppFreezingActivity
+import com.sameerasw.essentials.ui.activities.AppFreezingActivity
class AppFreezingTileService : BaseTileService() {
diff --git a/app/src/main/java/com/sameerasw/essentials/AppFreezingActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/AppFreezingActivity.kt
similarity index 96%
rename from app/src/main/java/com/sameerasw/essentials/AppFreezingActivity.kt
rename to app/src/main/java/com/sameerasw/essentials/ui/activities/AppFreezingActivity.kt
index 8c509a7d..086f2382 100644
--- a/app/src/main/java/com/sameerasw/essentials/AppFreezingActivity.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/AppFreezingActivity.kt
@@ -1,15 +1,13 @@
-package com.sameerasw.essentials
+package com.sameerasw.essentials.ui.activities
-import android.content.Context
import android.content.Intent
+import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
-import androidx.activity.SystemBarStyle
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -30,7 +28,6 @@ import androidx.compose.material3.ToggleFloatingActionButtonDefaults.animateIcon
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
@@ -40,9 +37,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.animation.*
-import androidx.compose.animation.core.*
-import androidx.core.graphics.drawable.toBitmap
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -58,11 +52,13 @@ import com.sameerasw.essentials.viewmodels.MainViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
+import com.sameerasw.essentials.FeatureSettingsActivity
+import com.sameerasw.essentials.R
import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
-@OptIn(ExperimentalMaterial3Api::class, androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
class AppFreezingActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
@@ -70,7 +66,7 @@ class AppFreezingActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
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
new file mode 100644
index 00000000..2be44c17
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt
@@ -0,0 +1,507 @@
+package com.sameerasw.essentials.ui.activities
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel
+import androidx.compose.material3.carousel.rememberCarouselState
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.unit.dp
+import com.sameerasw.essentials.ui.components.ReusableTopAppBar
+import com.sameerasw.essentials.ui.theme.EssentialsTheme
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.setValue
+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.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import com.sameerasw.essentials.R
+import com.sameerasw.essentials.domain.diy.Action
+import com.sameerasw.essentials.domain.diy.Automation
+import com.sameerasw.essentials.domain.diy.DIYRepository
+import com.sameerasw.essentials.domain.diy.Trigger
+import com.sameerasw.essentials.domain.diy.State as DIYState
+import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer
+import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenu
+import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem
+import com.sameerasw.essentials.ui.components.pickers.SegmentedPicker
+import com.sameerasw.essentials.ui.components.sheets.DimWallpaperSettingsSheet
+import com.sameerasw.essentials.utils.HapticUtil
+
+class AutomationEditorActivity : ComponentActivity() {
+
+ companion object {
+ private const val EXTRA_AUTOMATION_ID = "automation_id"
+ private const val EXTRA_AUTOMATION_TYPE = "automation_type"
+
+ fun createIntent(context: Context, automationId: String): Intent {
+ return Intent(context, AutomationEditorActivity::class.java).apply {
+ putExtra(EXTRA_AUTOMATION_ID, automationId)
+ }
+ }
+
+ fun createIntent(context: Context, type: Automation.Type): Intent {
+ return Intent(context, AutomationEditorActivity::class.java).apply {
+ putExtra(EXTRA_AUTOMATION_TYPE, type.name)
+ }
+ }
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+
+ // Init repository
+ DIYRepository.init(applicationContext)
+
+ val automationId = intent.getStringExtra(EXTRA_AUTOMATION_ID)
+ val automationTypeStr = intent.getStringExtra(EXTRA_AUTOMATION_TYPE)
+
+ val existingAutomation = if (automationId != null) DIYRepository.getAutomation(automationId) else null
+ val isEditMode = existingAutomation != null
+
+ val automationType = if (isEditMode) {
+ existingAutomation?.type ?: Automation.Type.TRIGGER
+ } else {
+ try {
+ Automation.Type.valueOf(automationTypeStr ?: Automation.Type.TRIGGER.name)
+ } catch (e: Exception) {
+ Automation.Type.TRIGGER
+ }
+ }
+
+ val titleRes = if (isEditMode) R.string.diy_editor_edit_title else R.string.diy_editor_new_title
+
+ setContent {
+ EssentialsTheme {
+ val view = LocalView.current
+ var carouselState = rememberCarouselState { 2 } // 0: Trigger/State, 1: Actions
+
+ // State for selections
+ // Initialize with existing data or defaults
+ var selectedTrigger by remember { mutableStateOf(existingAutomation?.trigger) }
+ var selectedState by remember { mutableStateOf(existingAutomation?.state) }
+
+ // Actions
+ // For Trigger type
+ var selectedAction by remember { mutableStateOf(existingAutomation?.actions?.firstOrNull()) }
+
+ // For State type
+ var selectedInAction by remember { mutableStateOf(existingAutomation?.entryAction) }
+ var selectedOutAction by remember { mutableStateOf(existingAutomation?.exitAction) }
+
+ // Tab for State Actions
+ var selectedActionTab by remember { mutableIntStateOf(0) } // 0: In, 1: Out
+
+ // Menu State
+ var showMenu by remember { mutableStateOf(false) }
+
+ // Config Sheets
+ var showDimSettings by remember { mutableStateOf(false) }
+ var configAction by remember { mutableStateOf(null) }
+
+ // Validation
+ val isValid = when (automationType) {
+ Automation.Type.TRIGGER -> selectedTrigger != null && selectedAction != null
+ Automation.Type.STATE -> selectedState != null && (selectedInAction != null || selectedOutAction != null)
+ }
+
+ Scaffold(
+ containerColor = MaterialTheme.colorScheme.surfaceContainer,
+ topBar = {
+ ReusableTopAppBar(
+ title = titleRes,
+ hasBack = true,
+ isSmall = true,
+ onBackClick = { finish() },
+ actions = {
+ if (isEditMode) {
+ IconButton(onClick = { showMenu = true }) {
+ Icon(
+ painter = painterResource(R.drawable.rounded_more_vert_24),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurface
+ )
+ }
+
+ SegmentedDropdownMenu(
+ expanded = showMenu,
+ onDismissRequest = { showMenu = false }
+ ) {
+ SegmentedDropdownMenuItem(
+ text = { Text(stringResource(R.string.action_delete)) },
+ onClick = {
+ showMenu = false
+ DIYRepository.removeAutomation(existingAutomation!!.id)
+ finish()
+ },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(R.drawable.rounded_delete_24),
+ contentDescription = null
+ )
+ }
+ )
+ }
+ }
+ }
+ )
+ }
+ ) { innerPadding ->
+ val configuration = LocalConfiguration.current
+ val screenWidth = configuration.screenWidthDp.dp
+
+ Column(modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ ) {
+ HorizontalMultiBrowseCarousel(
+ state = carouselState,
+ preferredItemWidth = screenWidth,
+ itemSpacing = 4.dp,
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth(),
+ contentPadding = PaddingValues(horizontal = 18.dp)
+ ) { index ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .maskClip(MaterialTheme.shapes.extraLarge)
+ .background(MaterialTheme.colorScheme.background)
+ ) {
+ if (index == 0) {
+ // PAGE 0: Trigger or State Picker
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Text(
+ text = stringResource(if (automationType == Automation.Type.TRIGGER) R.string.diy_select_trigger else R.string.diy_select_state),
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.padding(horizontal = 12.dp)
+ )
+
+ RoundedCardContainer(spacing = 2.dp) {
+ if (automationType == Automation.Type.TRIGGER) {
+ val triggers = listOf(
+ Trigger.ScreenOff,
+ Trigger.ScreenOn,
+ Trigger.DeviceUnlock,
+ Trigger.ChargerConnected,
+ Trigger.ChargerDisconnected
+ )
+ triggers.forEach { trigger ->
+ EditorActionItem(
+ title = stringResource(trigger.title),
+ iconRes = trigger.icon,
+ isSelected = selectedTrigger == trigger,
+ isConfigurable = trigger.isConfigurable,
+ onClick = { selectedTrigger = trigger },
+ onSettingsClick = {
+ // Handle trigger settings if needed later
+ }
+ )
+ }
+ } else {
+ val states = listOf(
+ DIYState.Charging,
+ DIYState.ScreenOn
+ )
+ states.forEach { state ->
+ EditorActionItem(
+ title = stringResource(state.title),
+ iconRes = state.icon,
+ isSelected = selectedState == state,
+ onClick = { selectedState = state }
+ )
+ }
+ }
+ }
+ }
+ } else {
+ // PAGE 1: Action Picker
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.diy_select_action),
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.padding(horizontal = 12.dp)
+ )
+
+ if (automationType == Automation.Type.STATE) {
+ // Tabs for In/Out
+ val options = listOf(
+ stringResource(R.string.diy_in_action_label),
+ stringResource(R.string.diy_out_action_label)
+ )
+ SegmentedPicker(
+ items = options,
+ selectedItem = options[selectedActionTab],
+ onItemSelected = {
+ HapticUtil.performUIHaptic(view)
+ selectedActionTab = options.indexOf(it)
+ },
+ labelProvider = { it },
+ modifier = Modifier.fillMaxWidth(),
+ cornerShape = MaterialTheme.shapes.extraExtraLarge.bottomEnd
+ )
+ }
+
+ RoundedCardContainer(spacing = 2.dp) {
+ val actions = listOf(
+ Action.TurnOnFlashlight,
+ Action.TurnOffFlashlight,
+ Action.ToggleFlashlight,
+ Action.HapticVibration,
+ Action.DimWallpaper()
+ )
+
+ val currentSelection = when(automationType) {
+ Automation.Type.TRIGGER -> selectedAction
+ Automation.Type.STATE -> if (selectedActionTab == 0) selectedInAction else selectedOutAction
+ }
+
+ // None option
+ EditorActionItem(
+ title = stringResource(R.string.action_none),
+ iconRes = R.drawable.rounded_do_not_disturb_on_24,
+ isSelected = currentSelection == null,
+ onClick = {
+ when(automationType) {
+ Automation.Type.TRIGGER -> selectedAction = null
+ Automation.Type.STATE -> {
+ if (selectedActionTab == 0) selectedInAction = null
+ else selectedOutAction = null
+ }
+ }
+ }
+ )
+
+ actions.forEach { action ->
+ // Check if the current selection matches this action type and update 'action' with the selected values if so
+ // This ensures the loop variable 'action' doesn't overwrite the configured values in 'currentSelection'
+ val resolvedAction = if (currentSelection != null && currentSelection::class == action::class) currentSelection else action
+
+ EditorActionItem(
+ title = stringResource(resolvedAction.title),
+ iconRes = resolvedAction.icon,
+ isSelected = currentSelection != null && currentSelection::class == resolvedAction::class,
+ isConfigurable = resolvedAction.isConfigurable,
+ onClick = {
+ when(automationType) {
+ Automation.Type.TRIGGER -> selectedAction = resolvedAction
+ Automation.Type.STATE -> {
+ if (selectedActionTab == 0) selectedInAction = resolvedAction
+ else selectedOutAction = resolvedAction
+ }
+ }
+ },
+ onSettingsClick = {
+ if (resolvedAction is Action.DimWallpaper) {
+ configAction = resolvedAction
+ showDimSettings = true
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (showDimSettings && configAction != null) {
+ DimWallpaperSettingsSheet(
+ initialAction = configAction!!,
+ onDismiss = { showDimSettings = false },
+ onSave = { newAction ->
+ showDimSettings = false
+ // Update the selection with configured action
+ when(automationType) {
+ Automation.Type.TRIGGER -> selectedAction = newAction
+ Automation.Type.STATE -> {
+ if (selectedActionTab == 0) selectedInAction = newAction
+ else selectedOutAction = newAction
+ }
+ }
+ configAction = null
+ }
+ )
+ }
+
+ // Bottom Actions
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp, horizontal = 24.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Button(
+ onClick = {
+ HapticUtil.performVirtualKeyHaptic(view)
+ finish()
+ },
+ modifier = Modifier.weight(1f),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.surfaceBright,
+ contentColor = MaterialTheme.colorScheme.onSurface
+ )
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.rounded_close_24),
+ contentDescription = null,
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(modifier = Modifier.size(8.dp))
+ Text(stringResource(R.string.action_cancel))
+ }
+
+ Button(
+ onClick = {
+ HapticUtil.performVirtualKeyHaptic(view)
+ // Save logic
+ if (automationType == Automation.Type.TRIGGER) {
+ val newAutomation = Automation(
+ id = if (isEditMode) existingAutomation!!.id else java.util.UUID.randomUUID().toString(),
+ type = Automation.Type.TRIGGER,
+ trigger = selectedTrigger,
+ actions = listOfNotNull(selectedAction)
+ )
+ if (isEditMode) DIYRepository.updateAutomation(newAutomation) else DIYRepository.addAutomation(newAutomation)
+ } else {
+ val newAutomation = Automation(
+ id = if (isEditMode) existingAutomation!!.id else java.util.UUID.randomUUID().toString(),
+ type = Automation.Type.STATE,
+ state = selectedState,
+ entryAction = selectedInAction,
+ exitAction = selectedOutAction
+ )
+ if (isEditMode) DIYRepository.updateAutomation(newAutomation) else DIYRepository.addAutomation(newAutomation)
+ }
+ finish()
+ },
+ modifier = Modifier.weight(1f),
+ enabled = isValid
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.rounded_check_24),
+ contentDescription = null,
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(modifier = Modifier.size(8.dp))
+ Text(stringResource(R.string.action_save))
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun EditorActionItem(
+ title: String,
+ iconRes: Int,
+ isSelected: Boolean,
+ isConfigurable: Boolean = false,
+ onClick: () -> Unit,
+ onSettingsClick: () -> Unit = {},
+ modifier: Modifier = Modifier
+) {
+ val view = LocalView.current
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .clickable {
+ HapticUtil.performUIHaptic(view)
+ onClick()
+ }
+ .background(
+ color = MaterialTheme.colorScheme.surfaceBright,
+ shape = RoundedCornerShape(MaterialTheme.shapes.extraSmall.bottomEnd)
+ )
+ .padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ RadioButton(
+ selected = isSelected,
+ onClick = onClick
+ )
+
+ Icon(
+ painter = painterResource(id = iconRes),
+ contentDescription = title,
+ modifier = Modifier.size(24.dp),
+ tint = MaterialTheme.colorScheme.primary
+ )
+
+ Text(
+ text = title,
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.weight(1f),
+ color = if (isSelected) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ if (isSelected && isConfigurable) {
+ IconButton(onClick = onSettingsClick) {
+ Icon(
+ painter = painterResource(id = R.drawable.rounded_settings_24),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/ReusableTopAppBar.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/ReusableTopAppBar.kt
index 80c1b9e6..055fc628 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/components/ReusableTopAppBar.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/components/ReusableTopAppBar.kt
@@ -21,6 +21,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
+import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -50,20 +51,14 @@ fun ReusableTopAppBar(
scrollBehavior: TopAppBarScrollBehavior? = null,
subtitle: Any? = null, // Can be Int or String
isBeta: Boolean = false,
+ backIconRes: Int = R.drawable.rounded_arrow_back_24,
+ isSmall: Boolean = false,
actions: @Composable RowScope.() -> Unit = {}
) {
val collapsedFraction = scrollBehavior?.state?.collapsedFraction ?: 0f
collapsedFraction > 0.5f
- LargeFlexibleTopAppBar(
- colors = TopAppBarDefaults.topAppBarColors(
- containerColor = MaterialTheme.colorScheme.surfaceContainer
- ),
- modifier = Modifier.padding(horizontal = 8.dp),
- expandedHeight = if (subtitle != null) 200.dp else 160.dp,
- collapsedHeight = 64.dp,
-
- title = {
+ val titleContent: @Composable () -> Unit = {
val resolvedTitle = when (title) {
is Int -> stringResource(id = title)
is String -> title
@@ -138,8 +133,9 @@ fun ReusableTopAppBar(
}
}
}
- },
- navigationIcon = {
+ }
+
+ val navigationIconContent: @Composable () -> Unit = {
if (hasBack) {
val view = LocalView.current
IconButton(
@@ -153,14 +149,15 @@ fun ReusableTopAppBar(
modifier = Modifier.size(48.dp)
) {
Icon(
- painter = painterResource(id = R.drawable.rounded_arrow_back_24),
+ painter = painterResource(id = backIconRes),
contentDescription = stringResource(R.string.action_back),
modifier = Modifier.size(32.dp)
)
}
}
- },
- actions = {
+ }
+
+ val actionsContent: @Composable RowScope.() -> Unit = {
actions()
if (hasHelp) {
@@ -239,7 +236,31 @@ fun ReusableTopAppBar(
)
}
}
- },
- scrollBehavior = scrollBehavior
- )
+ }
+
+ if (isSmall) {
+ TopAppBar(
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainer
+ ),
+ modifier = Modifier.padding(horizontal = 8.dp),
+ title = titleContent,
+ navigationIcon = navigationIconContent,
+ actions = actionsContent,
+ scrollBehavior = scrollBehavior
+ )
+ } else {
+ LargeFlexibleTopAppBar(
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainer
+ ),
+ modifier = Modifier.padding(horizontal = 8.dp),
+ expandedHeight = if (subtitle != null) 200.dp else 160.dp,
+ collapsedHeight = 64.dp,
+ title = titleContent,
+ navigationIcon = navigationIconContent,
+ actions = actionsContent,
+ scrollBehavior = scrollBehavior
+ )
+ }
}
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/diy/AutomationItem.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/diy/AutomationItem.kt
new file mode 100644
index 00000000..c2aebb8f
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/ui/components/diy/AutomationItem.kt
@@ -0,0 +1,308 @@
+package com.sameerasw.essentials.ui.components.diy
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.clip
+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.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.sameerasw.essentials.R
+import com.sameerasw.essentials.domain.diy.Action
+import com.sameerasw.essentials.domain.diy.Automation
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.MenuDefaults
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.graphics.Color
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.unit.DpOffset
+import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer
+import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenu
+import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem
+import com.sameerasw.essentials.utils.HapticUtil
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun AutomationItem(
+ automation: Automation,
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit = {},
+ onDelete: () -> Unit = {},
+ onToggle: () -> Unit = {}
+) {
+
+ val view = LocalView.current
+ var showMenu by remember { mutableStateOf(false) }
+
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceBright
+ ),
+ shape = MaterialTheme.shapes.extraSmall,
+ modifier = modifier
+ .combinedClickable(
+ onClick = {
+ HapticUtil.performVirtualKeyHaptic(view)
+ onClick()
+ },
+ onLongClick = {
+ HapticUtil.performVirtualKeyHaptic(view)
+ showMenu = true
+ }
+ )
+ .alpha(if (automation.isEnabled) 1f else 0.5f)
+ ) {
+ Box {
+ // Dropdown Menu
+ // Dropdown Menu
+ SegmentedDropdownMenu(
+ expanded = showMenu,
+ onDismissRequest = { showMenu = false },
+ offset = DpOffset(0.dp, 0.dp),
+ ) {
+ val toggleText = if (automation.isEnabled) stringResource(R.string.action_disable) else stringResource(R.string.action_enable)
+ val toggleIcon = if (automation.isEnabled) R.drawable.rounded_close_24 else R.drawable.rounded_check_24
+
+ SegmentedDropdownMenuItem(
+ text = { Text(toggleText) },
+ onClick = {
+ showMenu = false
+ onToggle()
+ },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(toggleIcon),
+ contentDescription = null
+ )
+ }
+ )
+ SegmentedDropdownMenuItem(
+ text = { Text(stringResource(R.string.action_edit)) },
+ onClick = {
+ showMenu = false
+ onClick()
+ },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(R.drawable.rounded_edit_24),
+ contentDescription = null
+ )
+ }
+ )
+ SegmentedDropdownMenuItem(
+ text = { Text(stringResource(R.string.action_delete)) },
+ onClick = {
+ showMenu = false
+ onDelete()
+ },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(R.drawable.rounded_delete_24),
+ contentDescription = null
+ )
+ }
+ )
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ .height(IntrinsicSize.Min),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RoundedCardContainer(
+ cornerRadius = 18.dp,
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxHeight(),
+ ) {
+ val icon =
+ if (automation.type == Automation.Type.TRIGGER) automation.trigger?.icon else automation.state?.icon
+ val title =
+ if (automation.type == Automation.Type.TRIGGER) automation.trigger?.title else automation.state?.title
+
+ if (icon != null && title != null) {
+ Surface(
+ color = MaterialTheme.colorScheme.secondaryContainer,
+ shape = RoundedCornerShape(16.dp),
+ modifier = Modifier.fillMaxSize()
+ ) {
+ Row(
+ modifier = Modifier
+ .padding(12.dp)
+ .fillMaxHeight(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ painter = painterResource(id = icon),
+ contentDescription = null,
+ modifier = Modifier.size(24.dp),
+ tint = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+ Text(
+ text = stringResource(id = title),
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSecondaryContainer,
+ fontWeight = FontWeight.SemiBold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+ }
+ }
+
+
+ if (automation.type == Automation.Type.TRIGGER) {
+ // Separator Icon
+ Box(
+ modifier = Modifier
+ .size(24.dp)
+ .clip(CircleShape)
+ .padding(horizontal = 3.dp)
+ .background(MaterialTheme.colorScheme.primaryContainer),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.rounded_arrow_forward_24),
+ contentDescription = null,
+ modifier = Modifier.size(16.dp),
+ tint = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ }
+ } else {
+ Column(
+ modifier = Modifier.fillMaxHeight(),
+ verticalArrangement = Arrangement.SpaceAround,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Box(
+ modifier = Modifier
+ .size(24.dp)
+ .clip(CircleShape)
+ .padding(horizontal = 3.dp)
+ .background(MaterialTheme.colorScheme.primaryContainer),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.rounded_arrow_forward_24),
+ contentDescription = null,
+ modifier = Modifier.size(16.dp),
+ tint = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ }
+
+ Box(
+ modifier = Modifier
+ .size(24.dp)
+ .clip(CircleShape)
+ .padding(horizontal = 3.dp)
+ .background(MaterialTheme.colorScheme.primaryContainer),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.rounded_arrow_back_24),
+ contentDescription = null,
+ modifier = Modifier.size(16.dp),
+ tint = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ }
+ }
+ }
+
+ // Right Side: Actions (Weight 1 to fill space)
+
+ RoundedCardContainer(
+ cornerRadius = 18.dp,
+ modifier = Modifier
+ .weight(1f),
+ ) {
+ if (automation.type == Automation.Type.TRIGGER) {
+ automation.actions.forEach { action ->
+ ActionItem(action = action)
+ }
+ } else {
+ // State Actions (In/Out)
+ // In Action (Top)
+ automation.entryAction?.let { action ->
+ ActionItem(action = action)
+ }
+
+ // Out Action (Bottom)
+ automation.exitAction?.let { action ->
+ ActionItem(action = action)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun ActionItem(
+ action: Action,
+ modifier: Modifier = Modifier
+) {
+ Surface(
+ color = MaterialTheme.colorScheme.surfaceContainerHigh,
+ shape = RoundedCornerShape(4.dp),
+ modifier = modifier.fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier.padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ painter = painterResource(id = action.icon),
+ contentDescription = null,
+ modifier = Modifier.size(24.dp),
+ tint = MaterialTheme.colorScheme.onSurface
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+ Text(
+ text = stringResource(id = action.title),
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurface,
+ fontWeight = FontWeight.SemiBold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/menus/SegmentedMenu.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/menus/SegmentedMenu.kt
new file mode 100644
index 00000000..5ea57e20
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/ui/components/menus/SegmentedMenu.kt
@@ -0,0 +1,71 @@
+package com.sameerasw.essentials.ui.components.menus
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.MenuDefaults
+import androidx.compose.material3.MenuItemColors
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.dp
+import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer
+
+@Composable
+fun SegmentedDropdownMenu(
+ expanded: Boolean,
+ onDismissRequest: () -> Unit,
+ modifier: Modifier = Modifier,
+ offset: DpOffset = DpOffset(0.dp, 0.dp),
+ content: @Composable ColumnScope.() -> Unit
+) {
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = onDismissRequest,
+ modifier = modifier,
+ offset = offset,
+ containerColor = Color.Transparent,
+ tonalElevation = 0.dp,
+ shadowElevation = 0.dp
+ ) {
+ RoundedCardContainer(
+ cornerRadius = 16.dp,
+ spacing = 2.dp,
+ content = content
+ )
+ }
+}
+
+@Composable
+fun SegmentedDropdownMenuItem(
+ text: @Composable () -> Unit,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ leadingIcon: @Composable (() -> Unit)? = null,
+ trailingIcon: @Composable (() -> Unit)? = null,
+ enabled: Boolean = true,
+ colors: MenuItemColors = MenuDefaults.itemColors(
+ textColor = MaterialTheme.colorScheme.onSurface,
+ leadingIconColor = MaterialTheme.colorScheme.onSurface,
+ trailingIconColor = MaterialTheme.colorScheme.onSurface,
+ disabledTextColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
+ disabledLeadingIconColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
+ disabledTrailingIconColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
+ )
+) {
+ DropdownMenuItem(
+ text = text,
+ onClick = onClick,
+ modifier = modifier
+ .clip(MaterialTheme.shapes.extraSmall)
+ .background(MaterialTheme.colorScheme.surfaceContainerHigh),
+ leadingIcon = leadingIcon,
+ trailingIcon = trailingIcon,
+ enabled = enabled,
+ colors = colors
+ )
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/SegmentedPicker.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/SegmentedPicker.kt
index f95741b4..7e4c3a4a 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/SegmentedPicker.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/SegmentedPicker.kt
@@ -4,6 +4,8 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CornerBasedShape
+import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonGroupDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
@@ -24,14 +26,15 @@ fun SegmentedPicker(
selectedItem: T,
onItemSelected: (T) -> Unit,
labelProvider: (T) -> String,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
+ cornerShape: CornerSize = MaterialTheme.shapes.extraSmall.bottomEnd,
) {
val view = androidx.compose.ui.platform.LocalView.current
Row(
modifier = modifier
.background(
color = MaterialTheme.colorScheme.surfaceBright,
- shape = RoundedCornerShape(MaterialTheme.shapes.extraSmall.bottomEnd)
+ shape = RoundedCornerShape(cornerShape)
)
.padding(10.dp),
horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween),
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/DimWallpaperSettingsSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/DimWallpaperSettingsSheet.kt
new file mode 100644
index 00000000..6ce364fa
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/DimWallpaperSettingsSheet.kt
@@ -0,0 +1,170 @@
+package com.sameerasw.essentials.ui.components.sheets
+
+import androidx.compose.foundation.background
+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.ModalBottomSheet
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+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.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.containers.RoundedCardContainer
+import com.sameerasw.essentials.utils.HapticUtil
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DimWallpaperSettingsSheet(
+ initialAction: Action.DimWallpaper,
+ onDismiss: () -> Unit,
+ onSave: (Action.DimWallpaper) -> Unit
+) {
+ val view = LocalView.current
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+
+ var dimAmount by remember { mutableFloatStateOf(initialAction.dimAmount) }
+
+ ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ sheetState = sheetState,
+ containerColor = MaterialTheme.colorScheme.surfaceContainer,
+ dragHandle = null
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(24.dp)
+ .fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(24.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.diy_action_dim_wallpaper),
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+
+ // Permissions Info
+ RoundedCardContainer {
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.rounded_info_24),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Text(
+ text = stringResource(R.string.diy_dim_wallpaper_desc),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ // Permission Icons
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ // Shizuku Icon
+ Icon(
+ painter = painterResource(R.drawable.rounded_adb_24),
+ contentDescription = "Shizuku",
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ // Root Icon
+ Icon(
+ painter = painterResource(R.drawable.rounded_numbers_24),
+ contentDescription = "Root",
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+
+ // Slider
+ Column {
+ Text(
+ text = "Dim Amount: ${(dimAmount * 100).toInt()}%",
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Slider(
+ value = dimAmount,
+ onValueChange = {
+ dimAmount = it
+ HapticUtil.performUIHaptic(view)
+ },
+ valueRange = 0f..1f
+ )
+ }
+
+ // Buttons
+ 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(id = R.drawable.rounded_close_24),
+ contentDescription = null,
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(modifier = Modifier.size(8.dp))
+ Text(stringResource(R.string.action_cancel))
+ }
+
+ Button(
+ onClick = {
+ HapticUtil.performVirtualKeyHaptic(view)
+ onSave(initialAction.copy(dimAmount = dimAmount))
+ },
+ modifier = Modifier.weight(1f)
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.rounded_check_24),
+ contentDescription = null,
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(modifier = Modifier.size(8.dp))
+ Text(stringResource(R.string.action_save))
+ }
+ }
+ Spacer(modifier = Modifier.size(16.dp))
+ }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/NewAutomationSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/NewAutomationSheet.kt
new file mode 100644
index 00000000..656e5930
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/NewAutomationSheet.kt
@@ -0,0 +1,145 @@
+package com.sameerasw.essentials.ui.components.sheets
+
+import androidx.compose.foundation.clickable
+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.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.OutlinedIconButton
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.sameerasw.essentials.R
+import com.sameerasw.essentials.domain.diy.Automation
+import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun NewAutomationSheet(
+ onDismiss: () -> Unit,
+ onOptionSelected: (Automation.Type) -> Unit
+) {
+ ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
+ containerColor = MaterialTheme.colorScheme.surfaceContainer,
+ contentColor = MaterialTheme.colorScheme.onSurface
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 12.dp, start = 24.dp, end = 24.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 18.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = stringResource(R.string.diy_editor_new_title),
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.weight(1f)
+ )
+
+ OutlinedIconButton(
+ onClick = { /* TODO: Implement import */ },
+ enabled = false
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.rounded_download_24),
+ contentDescription = "Import Automation"
+ )
+ }
+ }
+
+ RoundedCardContainer {
+ // Trigger Option
+ AutomationTypeOption(
+ title = stringResource(R.string.diy_create_trigger_title),
+ description = stringResource(R.string.diy_create_trigger_desc),
+ iconRes = R.drawable.rounded_bolt_24,
+ onClick = { onOptionSelected(Automation.Type.TRIGGER) }
+ )
+
+ // State Option
+ AutomationTypeOption(
+ title = stringResource(R.string.diy_create_state_title),
+ description = stringResource(R.string.diy_create_state_desc),
+ iconRes = R.drawable.rounded_toggle_on_24,
+ onClick = { onOptionSelected(Automation.Type.STATE) }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun AutomationTypeOption(
+ title: String,
+ description: String,
+ iconRes: Int,
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit
+) {
+ Surface(
+ color = MaterialTheme.colorScheme.surfaceContainerHighest,
+ shape = RoundedCornerShape(4.dp),
+ modifier = modifier
+ .fillMaxWidth()
+ .clickable(onClick = onClick)
+ ) {
+ Row(
+ modifier = Modifier.padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Spacer(modifier = Modifier.width(8.dp))
+ Icon(
+ painter = painterResource(id = iconRes),
+ contentDescription = null,
+ modifier = Modifier.size(32.dp),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Spacer(modifier = Modifier.width(16.dp))
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Text(
+ text = description,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ Spacer(modifier = Modifier.width(8.dp))
+ Icon(
+ painter = painterResource(R.drawable.rounded_chevron_right_24),
+ contentDescription = null,
+ modifier = Modifier.padding(end = 4.dp).size(24.dp),
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/ComingSoonDIYScreen.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/ComingSoonDIYScreen.kt
deleted file mode 100644
index cc0a7020..00000000
--- a/app/src/main/java/com/sameerasw/essentials/ui/composables/ComingSoonDIYScreen.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package com.sameerasw.essentials.ui.composables
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import com.sameerasw.essentials.R
-
-@Composable
-fun ComingSoonDIYScreen(
- modifier: Modifier = Modifier
-) {
- Column(
- modifier = modifier
- .fillMaxSize()
- .padding(32.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
- ) {
- Icon(
- painter = painterResource(id = R.drawable.rounded_experiment_24),
- contentDescription = null,
- modifier = Modifier.size(80.dp),
- tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
- )
-
- Spacer(modifier = Modifier.height(24.dp))
-
- Text(
- text = stringResource(R.string.coming_soon_title),
- style = MaterialTheme.typography.headlineMedium,
- fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.onSurface
- )
-
- Spacer(modifier = Modifier.height(8.dp))
-
- Text(
- text = stringResource(R.string.coming_soon_desc),
- style = MaterialTheme.typography.bodyLarge,
- textAlign = TextAlign.Center,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
-
- Spacer(modifier = Modifier.height(32.dp))
-
- Text(
- text = stringResource(R.string.coming_soon_label),
- style = MaterialTheme.typography.labelLarge,
- color = MaterialTheme.colorScheme.primary,
- fontWeight = FontWeight.Bold
- )
- }
-}
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/DIYScreen.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/DIYScreen.kt
new file mode 100644
index 00000000..acb5d3a2
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/DIYScreen.kt
@@ -0,0 +1,123 @@
+package com.sameerasw.essentials.ui.composables
+
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.sameerasw.essentials.ui.components.diy.AutomationItem
+import com.sameerasw.essentials.viewmodels.DIYViewModel
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import com.sameerasw.essentials.R
+import com.sameerasw.essentials.ui.activities.AutomationEditorActivity
+import com.sameerasw.essentials.ui.components.sheets.NewAutomationSheet
+
+@Composable
+fun DIYScreen(
+ modifier: Modifier = Modifier,
+ viewModel: DIYViewModel = viewModel()
+) {
+ val context = LocalContext.current
+ val automations by viewModel.automations.collectAsState()
+ val focusManager = LocalFocusManager.current
+
+ var showNewAutomationSheet by remember { mutableStateOf(false) }
+
+ Box(modifier = modifier.fillMaxSize()) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .pointerInput(Unit) {
+ detectTapGestures(onTap = { focusManager.clearFocus() })
+ }
+ .padding(24.dp),
+ verticalArrangement = Arrangement.Top,
+ horizontalAlignment = Alignment.Start
+ ) {
+ if (automations.isEmpty()) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "No automations yet"
+ )
+ }
+ } else {
+ LazyColumn(
+ modifier = Modifier
+ .clip(RoundedCornerShape(24.dp)),
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ items(automations) { automation ->
+ AutomationItem(
+ automation = automation,
+ onClick = {
+ context.startActivity(AutomationEditorActivity.createIntent(context, automation.id))
+ },
+ onDelete = {
+ viewModel.deleteAutomation(automation.id)
+ },
+ onToggle = {
+ viewModel.toggleAutomation(automation.id)
+ }
+ )
+ }
+ }
+ }
+ }
+
+ // FAB
+ FloatingActionButton(
+ onClick = { showNewAutomationSheet = true },
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(bottom = 32.dp, end = 32.dp),
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.rounded_add_24),
+ contentDescription = stringResource(R.string.diy_editor_new_title)
+ )
+ }
+ }
+
+ if (showNewAutomationSheet) {
+ NewAutomationSheet(
+ onDismiss = { showNewAutomationSheet = false },
+ onOptionSelected = { type ->
+ showNewAutomationSheet = false
+ context.startActivity(AutomationEditorActivity.createIntent(context, type))
+ }
+ )
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/DIYViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/DIYViewModel.kt
new file mode 100644
index 00000000..323b8d84
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/DIYViewModel.kt
@@ -0,0 +1,35 @@
+package com.sameerasw.essentials.viewmodels
+
+import com.sameerasw.essentials.domain.diy.DIYRepository
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import com.sameerasw.essentials.domain.diy.Automation
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+
+class DIYViewModel(application: Application) : AndroidViewModel(application) {
+ private val repository = DIYRepository
+
+ init {
+ repository.init(application)
+ }
+
+ val automations: StateFlow> = repository.automations
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(5000),
+ initialValue = emptyList()
+ )
+
+ fun deleteAutomation(id: String) {
+ repository.removeAutomation(id)
+ }
+
+ fun toggleAutomation(id: String) {
+ repository.getAutomation(id)?.let { automation ->
+ repository.updateAutomation(automation.copy(isEnabled = !automation.isEnabled))
+ }
+ }
+}
diff --git a/app/src/main/res/drawable/outline_bubble_chart_24.xml b/app/src/main/res/drawable/outline_bubble_chart_24.xml
new file mode 100644
index 00000000..f733c97e
--- /dev/null
+++ b/app/src/main/res/drawable/outline_bubble_chart_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_arrows_outward_24.xml b/app/src/main/res/drawable/rounded_arrows_outward_24.xml
new file mode 100644
index 00000000..aedd03af
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_arrows_outward_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_battery_android_frame_3_24.xml b/app/src/main/res/drawable/rounded_battery_android_frame_3_24.xml
new file mode 100644
index 00000000..5857b3ba
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_battery_android_frame_3_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_battery_charging_60_24.xml b/app/src/main/res/drawable/rounded_battery_charging_60_24.xml
new file mode 100644
index 00000000..afe2b22b
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_battery_charging_60_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_bolt_24.xml b/app/src/main/res/drawable/rounded_bolt_24.xml
new file mode 100644
index 00000000..3d7c205e
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_bolt_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_check_24.xml b/app/src/main/res/drawable/rounded_check_24.xml
new file mode 100644
index 00000000..eb2564df
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_check_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_compare_arrows_24.xml b/app/src/main/res/drawable/rounded_compare_arrows_24.xml
new file mode 100644
index 00000000..4bee4974
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_compare_arrows_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/rounded_download_24.xml b/app/src/main/res/drawable/rounded_download_24.xml
new file mode 100644
index 00000000..786601a9
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_download_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/rounded_mobile_cancel_24.xml b/app/src/main/res/drawable/rounded_mobile_cancel_24.xml
new file mode 100644
index 00000000..fbe44d51
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_mobile_cancel_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_mobile_screensaver_24.xml b/app/src/main/res/drawable/rounded_mobile_screensaver_24.xml
new file mode 100644
index 00000000..4344fcb5
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_mobile_screensaver_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_mobile_unlock_24.xml b/app/src/main/res/drawable/rounded_mobile_unlock_24.xml
new file mode 100644
index 00000000..6cb81fdc
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_mobile_unlock_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_toggle_on_24.xml b/app/src/main/res/drawable/rounded_toggle_on_24.xml
new file mode 100644
index 00000000..13e6dd3b
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_toggle_on_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 76baa3ef..c805a70d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -263,6 +263,14 @@
Light up for notifications
Sound mode tile
QS tile to toggle sound mode
+
+
+ Trigger Automation
+ Schedule an action to trigger on an observation
+ State Automation
+ Schedule an action to execute based on the state of a condition in and out
+ New Automation
+ Edit Automation
Link actions
Handle links with multiple apps
Snooze system notifications
@@ -411,6 +419,44 @@
Do It Yourself
Something experimental is brewing here. Stay tuned for automations and advanced mods.
Coming Soon
+ Trigger
+ State
+ Action
+ In
+ Out
+ Automation
+ Screen Off
+ Screen On
+ Device Unlock
+ Charger Connected
+ Charger Disconnected
+
+ Charging
+ Screen On
+
+ Vibrate
+ Show Notification
+ Remove Notification
+ Turn On Flashlight
+ Turn Off Flashlight
+ Toggle Flashlight
+ Dim Wallpaper
+ This action requires Shizuku or Root to adjust system wallpaper dimming.
+ Select Trigger
+ Select State
+ Select Action
+ In Action
+ Out Action
+ Cancel
+ Save
+ Edit
+ Delete
+ Enable
+ Disable
+
+ Automation Service
+ Automations Active
+ Monitoring system events for your automations
Sameera Wijerathna
diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml
index cc4d807c..77b3812c 100644
--- a/app/src/main/res/xml/shortcuts.xml
+++ b/app/src/main/res/xml/shortcuts.xml
@@ -9,7 +9,7 @@
+ android:targetClass="com.sameerasw.essentials.ui.activities.AppFreezingActivity" />
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ee5dedc0..d269d688 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -15,6 +15,7 @@ ui = "1.10.0"
foundation = "1.10.0"
playServicesLocation = "21.3.0"
material = "1.13.0"
+material3 = "1.4.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -40,6 +41,7 @@ androidx-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" }
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" }
play-services-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "playServicesLocation" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }