Skip to content
Merged

DIY #92

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -97,4 +98,7 @@ dependencies {

// Google Maps & Location
implementation(libs.play.services.location)

// Kotlin Reflect for dynamic sealed class serialization
implementation(kotlin("reflect"))
}
19 changes: 18 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,22 @@
android:launchMode="singleInstance"
android:theme="@style/Theme.Essentials.FullScreenAlarm" />


<activity
android:name=".ui.activities.AutomationEditorActivity"
android:exported="false"
android:label="@string/diy_editor_new_title"
android:theme="@style/Theme.Essentials" />


<service
android:name=".services.LocationReachedService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="location" />

<activity
android:name=".AppFreezingActivity"
android:name=".ui.activities.AppFreezingActivity"
android:exported="true"
android:label="@string/freeze_activity_title"
android:theme="@style/Theme.Essentials">
Expand Down Expand Up @@ -370,6 +378,15 @@
android:exported="false"
android:foregroundServiceType="specialUse" />

<service
android:name=".services.automation.AutomationService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="specialUse">
<property android:name="android.app.property.FOREGROUND_SERVICE_TYPE_SPECIAL_USE_DESCRIPTION"
android:value="Automation Service" />
</service>

<service
android:name=".services.tiles.AppFreezingTileService"
android:icon="@drawable/rounded_mode_cool_24"
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/com/sameerasw/essentials/EssentialsApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ class EssentialsApp : Application() {
context = applicationContext
ShizukuUtils.initialize()
com.sameerasw.essentials.utils.LogManager.init(this)

// Init Automation
com.sameerasw.essentials.domain.diy.DIYRepository.init(this)
com.sameerasw.essentials.services.automation.AutomationManager.init(this)

val intentFilter = IntentFilter(Intent.ACTION_SCREEN_OFF)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(screenOffReceiver, intentFilter, Context.RECEIVER_EXPORTED)
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/sameerasw/essentials/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -279,7 +279,7 @@ class MainActivity : FragmentActivity() {
)
}
DIYTabs.DIY -> {
ComingSoonDIYScreen(
DIYScreen(
modifier = Modifier.padding(innerPadding)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ class SettingsRepository(private val context: Context) {
fun getAllConfigsAsJsonString(): String {
return try {
val allConfigs = mutableMapOf<String, Map<String, Map<String, Any>>>()
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)
Expand Down
53 changes: 53 additions & 0 deletions app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt
Original file line number Diff line number Diff line change
@@ -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<String>
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<String> = listOf("shizuku", "root")
override val isConfigurable: Boolean = true
}
}
Original file line number Diff line number Diff line change
@@ -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<Action> = emptyList(),
val entryAction: Action? = null,
val exitAction: Action? = null,
val isEnabled: Boolean = true
) {
enum class Type {
TRIGGER,
STATE
}
}
114 changes: 114 additions & 0 deletions app/src/main/java/com/sameerasw/essentials/domain/diy/DIYRepository.kt
Original file line number Diff line number Diff line change
@@ -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<List<Automation>>(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<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
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<Automation> = if (json != null) {
try {
val type = object : TypeToken<List<Automation>>() {}.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 }
}
}
22 changes: 22 additions & 0 deletions app/src/main/java/com/sameerasw/essentials/domain/diy/State.kt
Original file line number Diff line number Diff line change
@@ -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
}
}
38 changes: 38 additions & 0 deletions app/src/main/java/com/sameerasw/essentials/domain/diy/Trigger.kt
Original file line number Diff line number Diff line change
@@ -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<String>
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
}
}
Loading