Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
2657805
Add Favorite on android automotive (#1)
cddu33 Apr 15, 2026
65f3651
clean
cddu33 Apr 15, 2026
b9f75c1
improve detection car park
cddu33 Apr 15, 2026
ceebc75
correct favorite visible
cddu33 Apr 16, 2026
03b9cc0
beta publication playstore
cddu33 Apr 16, 2026
1e42075
Merge branch 'main' into feature/add-favorite-automotive
cddu33 Apr 16, 2026
251f8a2
Update app/src/main/kotlin/io/homeassistant/companion/android/vehicle…
cddu33 Apr 16, 2026
74cc6f1
Update common/src/main/res/values/strings.xml
cddu33 Apr 16, 2026
e2cd297
Update app/src/main/kotlin/io/homeassistant/companion/android/vehicle…
cddu33 Apr 16, 2026
82f2143
Update app/src/main/kotlin/io/homeassistant/companion/android/vehicle…
cddu33 Apr 16, 2026
ce72bb6
Update app/src/main/kotlin/io/homeassistant/companion/android/vehicle…
cddu33 Apr 16, 2026
2bb7210
Update app/src/main/kotlin/io/homeassistant/companion/android/vehicle…
cddu33 Apr 16, 2026
f39eaf4
Merge branch 'feature/add-favorite-automotive' of https://github.com/…
cddu33 Apr 16, 2026
df516d2
clean logs
cddu33 Apr 16, 2026
d43b4aa
Update .idea/markdown.xml
cddu33 Apr 16, 2026
a473bc0
clean logs
cddu33 Apr 16, 2026
13129c8
Merge branch 'feature/add-favorite-automotive' of https://github.com/…
cddu33 Apr 16, 2026
b65ce01
Revert "beta publication playstore" (#2)
cddu33 Apr 16, 2026
ee5a158
clean logs
cddu33 Apr 16, 2026
622c276
Update app/src/main/kotlin/io/homeassistant/companion/android/vehicle…
cddu33 Apr 16, 2026
6da3ba5
delete unused val
cddu33 Apr 16, 2026
62465f1
Merge remote-tracking branch 'origin/feature/add-favorite-automotive'…
cddu33 Apr 16, 2026
4af0bf0
Copilot suggestion (#3)
cddu33 Apr 16, 2026
2df7f86
revert beta
cddu33 Apr 16, 2026
51bd2d3
revert beta
cddu33 Apr 16, 2026
b413653
removed unused import
cddu33 Apr 16, 2026
0ed7c46
feature/add-favorite-automotive (#4)
cddu33 Apr 16, 2026
3b44395
clean import
cddu33 Apr 16, 2026
0a74259
Feature/add favorite automotive (#5)
cddu33 Apr 16, 2026
2782057
Reorder imports in DomainListScreen.kt
cddu33 Apr 16, 2026
9c2ca73
Update DomainListScreen.kt
cddu33 Apr 16, 2026
d9e331b
clean @API
cddu33 Apr 16, 2026
80bcfc0
clean @api
cddu33 Apr 16, 2026
10f6bf9
clean @api
cddu33 Apr 16, 2026
b622996
beta
cddu33 Apr 16, 2026
e91c50c
Merge remote-tracking branch 'origin/beta_playsotre' into feature/add…
cddu33 Apr 16, 2026
1fed001
Merge branch 'beta_playsotre' into feature/add-favorite-automotive
cddu33 Apr 16, 2026
dea98ca
beta
cddu33 Apr 16, 2026
02dffc4
Merge remote-tracking branch 'origin/feature/add-favorite-automotive'…
cddu33 Apr 16, 2026
17cf7ad
Merge branch 'main' into feature/add-favorite-automotive
cddu33 Apr 16, 2026
9bab495
Revert "beta"
clementD33 Apr 20, 2026
e395938
Merge branch 'main' into feature/add-favorite-automotive
clementD33 Apr 20, 2026
00c7879
remove unused import
cddu33 Apr 20, 2026
86b234e
correct src/main/kotlin/io/homeassistant/companion/android/util/ve…
cddu33 Apr 20, 2026
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
8 changes: 8 additions & 0 deletions .idea/markdown.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import io.homeassistant.companion.android.util.RegistriesDataHandler
import io.homeassistant.companion.android.vehicle.ChangeServerScreen
import io.homeassistant.companion.android.vehicle.DomainListScreen
import io.homeassistant.companion.android.vehicle.EntityGridVehicleScreen
import io.homeassistant.companion.android.vehicle.ManageFavoritesVehicleScreen
import io.homeassistant.companion.android.vehicle.MapVehicleScreen
import java.time.LocalDateTime
import java.util.Locale
Expand Down Expand Up @@ -256,3 +257,32 @@ fun getDomainsGridItem(
}
}
}

/**
* Creates a header [Action] that opens the [ManageFavoritesVehicleScreen], allowing the user
* to add or remove entities from the automotive favorites list. Intended for use in the header
* of automotive screens when the vehicle is parked.
*/
@RequiresApi(Build.VERSION_CODES.O)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
fun getManageFavoritesAction(
carContext: CarContext,
screenManager: ScreenManager,
serverId: StateFlow<Int>,
allEntities: Flow<Map<String, Entity>>,
prefsRepository: PrefsRepository,
): Action {
return Action.Builder()
.setTitle(carContext.getString(R.string.aa_manage_favorites))
.setOnClickListener {
Timber.i("Manage favorites clicked")
screenManager.push(
ManageFavoritesVehicleScreen(
carContext,
serverId,
allEntities,
prefsRepository,
),
)
}
.build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,10 @@ abstract class BaseVehicleScreen(carContext: CarContext) : Screen(carContext) {
private var car: Car? = null
private var carRestrictionManager: CarUxRestrictionsManager? = null
protected val isDrivingOptimized
get() = try {
(car?.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE) as? CarUxRestrictionsManager)
?.currentCarUxRestrictions
?.isRequiresDistractionOptimization
?: false
} catch (e: Exception) {
Timber.e(e, "Error getting UX Restrictions")
false
}
get() = carRestrictionManager
?.currentCarUxRestrictions
?.isRequiresDistractionOptimization
?: false

init {
lifecycle.addObserver(object : DefaultLifecycleObserver {
Expand All @@ -41,16 +36,22 @@ abstract class BaseVehicleScreen(carContext: CarContext) : Screen(carContext) {
abstract fun onDrivingOptimizedChanged(newState: Boolean)

private fun registerAutomotiveRestrictionListener() {
Timber.d("registerAutomotiveRestrictionListener: isAutomotive=${carContext.isAutomotive()}")
if (carContext.isAutomotive()) {
Timber.i("Register for Automotive Restrictions")
car = Car.createCar(carContext)
carRestrictionManager =
car?.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE) as? CarUxRestrictionsManager
Timber.d("carRestrictionManager=$carRestrictionManager")
val initialState = carRestrictionManager?.currentCarUxRestrictions?.isRequiresDistractionOptimization ?: false
Timber.d("Initial isDrivingOptimized=$initialState")
val listener =
CarUxRestrictionsManager.OnUxRestrictionsChangedListener { restrictions ->
Timber.d("UxRestrictions changed: isRequiresDistractionOptimization=${restrictions.isRequiresDistractionOptimization}")
onDrivingOptimizedChanged(restrictions.isRequiresDistractionOptimization)
Comment thread
cddu33 marked this conversation as resolved.
Outdated
}
carRestrictionManager?.registerListener(listener)
invalidate()
Comment thread
cddu33 marked this conversation as resolved.
Outdated
Comment thread
cddu33 marked this conversation as resolved.
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import io.homeassistant.companion.android.util.vehicle.SUPPORTED_DOMAINS
import io.homeassistant.companion.android.util.vehicle.getDomainList
import io.homeassistant.companion.android.util.vehicle.getHeaderBuilder
import io.homeassistant.companion.android.util.vehicle.nativeModeAction
import io.homeassistant.companion.android.util.vehicle.getManageFavoritesAction
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -71,8 +72,20 @@ class DomainListScreen(

return GridTemplate.Builder().apply {
val headerBuilder = carContext.getHeaderBuilder(R.string.all_entities)
if (isAutomotive && !isDrivingOptimized && BuildConfig.FLAVOR != "full") {
headerBuilder.addEndHeaderAction(nativeModeAction(carContext))
if (isAutomotive && !isDrivingOptimized) {
if (BuildConfig.FLAVOR != "full") {
headerBuilder.addEndHeaderAction(nativeModeAction(carContext))
} else {
headerBuilder.addEndHeaderAction(
getManageFavoritesAction(
carContext,
screenManager,
serverId,
allEntities,
prefsRepository
Comment thread
cddu33 marked this conversation as resolved.
Outdated
),
)
}
}
setHeader(headerBuilder.build())
val domainBuild = domainList.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import io.homeassistant.companion.android.util.vehicle.SUPPORTED_DOMAINS
import io.homeassistant.companion.android.util.vehicle.getChangeServerGridItem
import io.homeassistant.companion.android.util.vehicle.getDomainList
import io.homeassistant.companion.android.util.vehicle.getHeaderBuilder
import io.homeassistant.companion.android.util.vehicle.getManageFavoritesAction
import io.homeassistant.companion.android.util.vehicle.getNavigationGridItem
import io.homeassistant.companion.android.util.vehicle.nativeModeAction
import kotlinx.coroutines.CancellationException
Expand Down Expand Up @@ -217,8 +218,21 @@ class MainVehicleScreen(
}.build()

val headerBuilder = carContext.getHeaderBuilder(commonR.string.app_name, Action.APP_ICON)
if (isAutomotive && !isDrivingOptimized && BuildConfig.FLAVOR != "full") {
headerBuilder.addEndHeaderAction(nativeModeAction(carContext))
Timber.d("onGetTemplate: isAutomotive=$isAutomotive isDrivingOptimized=$isDrivingOptimized")
if (isAutomotive && !isDrivingOptimized) {
Comment thread
cddu33 marked this conversation as resolved.
Outdated
if (BuildConfig.FLAVOR != "full") {
headerBuilder.addEndHeaderAction(nativeModeAction(carContext))
} else {
headerBuilder.addEndHeaderAction(
getManageFavoritesAction(
carContext,
screenManager,
serverId,
allEntities,
prefsRepository
Comment thread
cddu33 marked this conversation as resolved.
Outdated
),
)
}
}
headerBuilder.addEndHeaderAction(refreshAction)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package io.homeassistant.companion.android.vehicle

import android.os.Build
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
import androidx.annotation.RequiresApi
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
import androidx.car.app.CarContext
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.car.app.model.Toggle
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import io.homeassistant.companion.android.common.R as commonR
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.prefs.AutoFavorite
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
import io.homeassistant.companion.android.util.vehicle.SUPPORTED_DOMAINS_WITH_STRING
import io.homeassistant.companion.android.util.vehicle.getHeaderBuilder
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

/**
* A Car App screen that allows users to manage their automotive favorites when the vehicle is
* parked. Each entity from the supported domains is displayed with a toggle to add or remove
* it from the favorites list. Current favorites are sorted to the top.
*
* This screen stays fully within the Car App API, making it compliant with Play Store
* automotive distribution policies.
*/
@RequiresApi(Build.VERSION_CODES.O)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
class ManageFavoritesVehicleScreen(
carContext: CarContext,
private val serverId: StateFlow<Int>,
private val allEntities: Flow<Map<String, Entity>>,
private val prefsRepository: PrefsRepository,
) : BaseVehicleScreen(carContext) {

private var entities: List<Entity> = emptyList()
private var favoritesList: List<AutoFavorite> = emptyList()
private var isLoaded = false
private var page = 0

init {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
favoritesList = prefsRepository.getAutoFavorites()
allEntities.collect { entityMap ->
val newEntities = entityMap.values
.filter { it.domain in SUPPORTED_DOMAINS_WITH_STRING }
.sortedWith(
compareByDescending<Entity> { entity ->
favoritesList.any {
it.serverId == serverId.value && it.entityId == entity.entityId
}
Comment thread
cddu33 marked this conversation as resolved.
Outdated
}.thenBy { it.attributes["friendly_name"]?.toString() ?: it.entityId },
)
if (newEntities.map { it.entityId } != entities.map { it.entityId }) {
page = 0
}
entities = newEntities
isLoaded = true
invalidate()
}
}
}
}

override fun onDrivingOptimizedChanged(newState: Boolean) {
if (newState) {
lifecycleScope.launch {
Timber.i("Fermeture de l'écran car la voiture roule")
screenManager.pop()
Comment thread
cddu33 marked this conversation as resolved.
}
}
invalidate()
}

override fun onGetTemplate(): Template {
val listLimit = carContext.getCarService(ConstraintManager::class.java)
.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)

val itemsPerPage = (listLimit - 2).coerceAtLeast(1)

val fromIndex = page * itemsPerPage
val toIndex = minOf(fromIndex + itemsPerPage, entities.size)
val hasPreviousPage = page > 0
val hasNextPage = toIndex < entities.size
Comment thread
cddu33 marked this conversation as resolved.
Outdated
val pageEntities = if (isLoaded) entities.subList(fromIndex, toIndex) else emptyList()
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

entities.subList(fromIndex, toIndex) can throw if fromIndex > entities.size (e.g., if page is out of range after the entity list changes). Guard against out-of-range paging (clamp page, or return an empty list when fromIndex >= entities.size) before calling subList.

Suggested change
val pageEntities = if (isLoaded) entities.subList(fromIndex, toIndex) else emptyList()
val pageEntities = if (isLoaded && fromIndex < entities.size) {
entities.subList(fromIndex, toIndex)
} else {
emptyList()
}

Copilot uses AI. Check for mistakes.

val listBuilder = ItemList.Builder()

if (hasPreviousPage) {
listBuilder.addItem(
Row.Builder()
.setTitle(carContext.getString(commonR.string.aa_previous_page))
.setOnClickListener {
page--
invalidate()
}
.build(),
)
}

pageEntities.forEach { entity ->
val isFavorite = favoritesList.any {
it.serverId == serverId.value && it.entityId == entity.entityId
}
val friendlyName = entity.attributes["friendly_name"]?.toString() ?: entity.entityId
val domainLabel = SUPPORTED_DOMAINS_WITH_STRING[entity.domain]
?.let { carContext.getString(it) }
?: entity.domain

listBuilder.addItem(
Row.Builder()
.setTitle(friendlyName)
.addText(domainLabel)
.setEnabled(!isDrivingOptimized)
.setToggle(
Toggle.Builder { isChecked ->
lifecycleScope.launch {
val favorite = AutoFavorite(
serverId = serverId.value,
entityId = entity.entityId,
)
if (isChecked) {
Timber.d("Adding favorite: ${entity.entityId}")
prefsRepository.addAutoFavorite(favorite)
} else {
Timber.d("Removing favorite: ${entity.entityId}")
Comment thread
cddu33 marked this conversation as resolved.
Outdated
val updated = favoritesList.filterNot { it == favorite }
Comment thread
cddu33 marked this conversation as resolved.
prefsRepository.setAutoFavorites(updated)
}
favoritesList = prefsRepository.getAutoFavorites()
Comment thread
cddu33 marked this conversation as resolved.
invalidate()
}
}
.setChecked(isFavorite)
.build(),
)
.build(),
)
}

if (hasNextPage) {
listBuilder.addItem(
Row.Builder()
.setTitle(carContext.getString(commonR.string.aa_next_page))
.setOnClickListener {
page++
invalidate()
}
.build(),
)
}

if (isLoaded && entities.isEmpty()) {
listBuilder.setNoItemsMessage(carContext.getString(commonR.string.no_supported_entities))
}

return ListTemplate.Builder()
.setHeader(carContext.getHeaderBuilder(commonR.string.android_automotive_favorites).build())
.setLoading(!isLoaded)
.apply { if (isLoaded) setSingleList(listBuilder.build()) }
.build()
}
}
4 changes: 4 additions & 0 deletions common/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,10 @@
<string name="aa_favorites_summary">Select your favorite entities to be shown in the app while viewing the Home Assistant driving interface</string>
<string name="android_automotive">Android Automotive</string>
<string name="android_automotive_favorites">Driving favorites</string>
<string name="aa_manage_favorites">Manage favorites</string>
<string name="aa_manage_favorites_parked_only">Managing favorites is only available when parked</string>
Comment thread
cddu33 marked this conversation as resolved.
Outdated
<string name="aa_previous_page">← Previous page</string>
<string name="aa_next_page">Next page →</string>
<string name="alarm_control_panels">Alarm Control Panels</string>
<string name="state_triggered">Triggered</string>
<string name="state_disarmed">Disarmed</string>
Expand Down
Loading