Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,18 @@ sealed interface Trigger {
override val icon: Int = R.drawable.rounded_do_not_disturb_on_24
}

@Keep
data object AndroidAutoConnected : Trigger {
override val title: Int = R.string.diy_trigger_android_auto_connected
override val icon: Int = R.drawable.rounded_directions_car_24
}

@Keep
data object AndroidAutoDisconnected : Trigger {
override val title: Int = R.string.diy_trigger_android_auto_disconnected
override val icon: Int = R.drawable.rounded_directions_car_24
}

companion object {
const val DEFAULT_LOCATION_RADIUS_METERS = 200
const val MIN_LOCATION_RADIUS_METERS = 100
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ data class AutomationSuggestion(
"BluetoothDisconnected",
"WifiConnected",
"WifiDisconnected",
"AndroidAutoConnected",
"AndroidAutoDisconnected",
],
)
val triggerType: String? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ object GenAIAutomationService {
{
"type": "TRIGGER" or "STATE" or "APP",
"title": "Short descriptive title",
"triggerType": "ScreenOff" or "ScreenOn" or "DeviceUnlock" or "ChargerConnected" or "ChargerDisconnected" or "PowerSavingOn" or "PowerSavingOff" or "Schedule" or "BluetoothConnected" or "BluetoothDisconnected" or "WifiConnected" or "WifiDisconnected",
"triggerType": "ScreenOff" or "ScreenOn" or "DeviceUnlock" or "ChargerConnected" or "ChargerDisconnected" or "PowerSavingOn" or "PowerSavingOff" or "Schedule" or "BluetoothConnected" or "BluetoothDisconnected" or "WifiConnected" or "WifiDisconnected" or "AndroidAutoConnected" or "AndroidAutoDisconnected",
"stateType": "Charging" or "ScreenOn" or "PowerSaving" or "TimePeriod",
"actionTypes": ["HapticVibration", "ShowNotification", "RemoveNotification", "TurnOnFlashlight", "TurnOffFlashlight", "ToggleFlashlight", "DimWallpaper", "DeviceEffects", "SoundMode", "TurnOnLowPower", "TurnOffLowPower", "ScreenOff", "MediaPlayPause", "MediaNext", "MediaPrevious", "AIAssistant", "TakeScreenshot", "ToggleMediaVolume", "LikeCurrentSong", "OpenNowPlayingApp", "CircleToSearch", "PinApp", "SplitScreen", "SometimesEssentials", "FreezeTag"],
"explanation": "Brief 1-2 sentence description of what this automation does",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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.services.automation.executors.CombinedActionExecutor
import com.sameerasw.essentials.services.automation.modules.AndroidAutoModule
import com.sameerasw.essentials.services.automation.modules.AutomationModule
import com.sameerasw.essentials.services.automation.modules.BluetoothModule
import com.sameerasw.essentials.services.automation.modules.DisplayModule
Expand Down Expand Up @@ -116,6 +117,7 @@ object AutomationManager {
val calendarAutomations = mutableListOf<Automation>()
val notificationAutomations = mutableListOf<Automation>()
val locationAutomations = mutableListOf<Automation>()
val androidAutoAutomations = mutableListOf<Automation>()

enabledAutomations.forEach { automation ->
when (automation.type) {
Expand Down Expand Up @@ -156,6 +158,11 @@ object AutomationManager {
locationAutomations.add(automation)
}

is Trigger.AndroidAutoConnected, is Trigger.AndroidAutoDisconnected -> {
requiredModuleIds.add(AndroidAutoModule.ID)
androidAutoAutomations.add(automation)
}

else -> {}
}
}
Expand Down Expand Up @@ -298,6 +305,16 @@ object AutomationManager {
} else {
activeModules.remove(LocationModule.ID)?.stop(context)
}

if (requiredModuleIds.contains(AndroidAutoModule.ID)) {
val module =
activeModules.getOrPut(AndroidAutoModule.ID) {
AndroidAutoModule().also { it.start(context) }
}
module.updateAutomations(androidAutoAutomations)
} else {
activeModules.remove(AndroidAutoModule.ID)?.stop(context)
}
}

private fun startService(context: Context) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2026 sameerasw.com
* License: MIT License
*
* Feature Module: Background Services & Receivers
* File: AndroidAutoModule.kt
* Description: Fires automations when Android Auto starts or stops projecting.
*/

package com.sameerasw.essentials.services.automation.modules

import android.content.Context
import com.sameerasw.essentials.domain.diy.Automation
import com.sameerasw.essentials.domain.diy.Trigger
import com.sameerasw.essentials.services.automation.executors.CombinedActionExecutor
import com.sameerasw.essentials.utils.CarConnectionMonitor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch

class AndroidAutoModule : AutomationModule {
companion object {
const val ID = "android_auto_module"
}

override val id: String = ID
private var automations: List<Automation> = emptyList()
private val scope = CoroutineScope(Dispatchers.IO)
private var appContext: Context? = null
private var isProjecting = false

private val listener: (Boolean) -> Unit = { projecting ->
if (projecting != isProjecting) {
isProjecting = projecting
appContext?.let {
handleTrigger(it, if (projecting) Trigger.AndroidAutoConnected else Trigger.AndroidAutoDisconnected)
}
}
}

override fun start(context: Context) {
appContext = context.applicationContext
isProjecting = CarConnectionMonitor.isProjecting
CarConnectionMonitor.addListener(listener)
}

override fun stop(context: Context) {
CarConnectionMonitor.removeListener(listener)
scope.coroutineContext.cancelChildren()
appContext = null
}

override fun updateAutomations(automations: List<Automation>) {
this.automations = automations
}

private fun handleTrigger(
context: Context,
trigger: Trigger,
) {
scope.launch {
automations
.filter { it.type == Automation.Type.TRIGGER && it.trigger == trigger }
.forEach { automation ->
CombinedActionExecutor.executeAll(context, automation.actionList)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,8 @@ class AutomationEditorActivity : ComponentActivity() {
ssid =
(selectedTrigger as? Trigger.WifiDisconnected)?.ssid ?: "",
),
Trigger.AndroidAutoConnected,
Trigger.AndroidAutoDisconnected,
),
R.string.diy_category_notifications to
listOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ class DIYViewModel(
"BluetoothDisconnected" -> Trigger.BluetoothDisconnected()
"WifiConnected" -> Trigger.WifiConnected()
"WifiDisconnected" -> Trigger.WifiDisconnected()
"AndroidAutoConnected" -> Trigger.AndroidAutoConnected
"AndroidAutoDisconnected" -> Trigger.AndroidAutoDisconnected
else -> if (type == Automation.Type.TRIGGER) Trigger.ScreenOff else null
}

Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1343,6 +1343,8 @@
<string name="diy_notification_trigger_app">App</string>
<string name="diy_notification_trigger_any_app">Any app</string>
<string name="diy_notification_trigger_text">Text contains (optional)</string>
<string name="diy_trigger_android_auto_connected">Android Auto Connected</string>
<string name="diy_trigger_android_auto_disconnected">Android Auto Disconnected</string>
<string name="diy_trigger_accessibility_button">Accessibility Shortcut</string>
<string name="accessibility_shortcut_service_name">Essentials Shortcut</string>
<string name="accessibility_shortcut_service_1_name">Essentials Shortcut 1</string>
Expand Down
Loading