From 26578052f1b501eef70919b0b4479306d59cfab3 Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 16 Apr 2026 00:05:20 +0200 Subject: [PATCH 01/49] Add Favorite on android automotive (#1) --- .idea/markdown.xml | 8 + .../util/vehicle/TemplateComponents.kt | 30 +++ .../android/vehicle/DomainListScreen.kt | 17 +- .../android/vehicle/MainVehicleScreen.kt | 17 +- .../vehicle/ManageFavoritesVehicleScreen.kt | 193 ++++++++++++++++++ common/src/main/res/values/strings.xml | 5 + 6 files changed, 266 insertions(+), 4 deletions(-) create mode 100644 .idea/markdown.xml create mode 100644 app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt diff --git a/.idea/markdown.xml b/.idea/markdown.xml new file mode 100644 index 00000000000..c61ea3346e8 --- /dev/null +++ b/.idea/markdown.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt b/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt index 19bbc15b345..6ceb8c7716f 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt @@ -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 @@ -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) +fun getManageFavoritesAction( + carContext: CarContext, + screenManager: ScreenManager, + serverId: StateFlow, + allEntities: Flow>, + 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() +} diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt index 97ee1d91460..52059a32c6f 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt @@ -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 @@ -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 + ), + ) + } } setHeader(headerBuilder.build()) val domainBuild = domainList.build() diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt index c1d2d760cc0..b59cfa0717b 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt @@ -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 @@ -217,8 +218,20 @@ class MainVehicleScreen( }.build() val headerBuilder = carContext.getHeaderBuilder(commonR.string.app_name, Action.APP_ICON) - 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 + ), + ) + } } headerBuilder.addEndHeaderAction(refreshAction) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt new file mode 100644 index 00000000000..61d8cc41429 --- /dev/null +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -0,0 +1,193 @@ +package io.homeassistant.companion.android.vehicle + +import android.os.Build +import androidx.annotation.RequiresApi +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.car.app.model.Action +import androidx.car.app.model.CarIcon +import androidx.car.app.model.MessageTemplate +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 + +/** + * 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) +class ManageFavoritesVehicleScreen( + carContext: CarContext, + private val serverId: StateFlow, + private val allEntities: Flow>, + private val prefsRepository: PrefsRepository, +) : BaseVehicleScreen(carContext) { + + private var entities: List = emptyList() + private var favoritesList: List = 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 -> + favoritesList.any { + it.serverId == serverId.value && it.entityId == entity.entityId + } + }.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) { + Timber.d("Etat conduite : $newState") + if (newState) { + // On utilise le scope lié au cycle de vie pour revenir sur le thread UI + lifecycleScope.launch { + Timber.i("Fermeture de l'écran car la voiture roule") + screenManager.pop() + } + } + invalidate() + } + + override fun onGetTemplate(): Template { + // LOGIQUE INVERSE : Si on roule, on affiche l'écran de restriction + if (!isDrivingOptimized) { + return MessageTemplate.Builder( + carContext.getString(commonR.string.no_favorites_while_driving) + ) + .setHeader(carContext.getHeaderBuilder(commonR.string.android_automotive_favorites).build()) + .setIcon(CarIcon.ERROR) // Utilise l'icône d'alerte + .addAction( + Action.Builder() + .setTitle(carContext.getString(commonR.string.close)) + .setOnClickListener { screenManager.pop() } + .build() + ) + .build() + } + + val listLimit = carContext.getCarService(ConstraintManager::class.java) + .getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST) + + // Always reserve 2 rows for navigation (Previous + Next) to keep a fixed itemsPerPage + // across all pages, avoiding index drift when navigating back and forth. + 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 + val pageEntities = if (isLoaded) entities.subList(fromIndex, toIndex) else emptyList() + + 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}") + val updated = favoritesList.filterNot { it == favorite } + prefsRepository.setAutoFavorites(updated) + } + favoritesList = prefsRepository.getAutoFavorites() + 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() + } +} diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 51570d8d6e6..eacf68932d6 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1235,6 +1235,10 @@ Select your favorite entities to be shown in the app while viewing the Home Assistant driving interface Android Automotive Driving favorites + Manage favorites + Managing favorites is only available when parked + ← Previous page + Next page → Alarm Control Panels Triggered Disarmed @@ -1469,4 +1473,5 @@ No app available to open %s Experimental Learn more + " Not Allow on Driving" From 65f36513b4f3bd25f438874aac6dfb179d2bf0d0 Mon Sep 17 00:00:00 2001 From: cddu33 Date: Thu, 16 Apr 2026 00:11:04 +0200 Subject: [PATCH 02/49] clean --- .../vehicle/ManageFavoritesVehicleScreen.kt | 21 ------------------- common/src/main/res/values/strings.xml | 1 - 2 files changed, 22 deletions(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index 61d8cc41429..f5455d67c3f 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -9,9 +9,6 @@ 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.car.app.model.Action -import androidx.car.app.model.CarIcon -import androidx.car.app.model.MessageTemplate import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -85,27 +82,9 @@ class ManageFavoritesVehicleScreen( } override fun onGetTemplate(): Template { - // LOGIQUE INVERSE : Si on roule, on affiche l'écran de restriction - if (!isDrivingOptimized) { - return MessageTemplate.Builder( - carContext.getString(commonR.string.no_favorites_while_driving) - ) - .setHeader(carContext.getHeaderBuilder(commonR.string.android_automotive_favorites).build()) - .setIcon(CarIcon.ERROR) // Utilise l'icône d'alerte - .addAction( - Action.Builder() - .setTitle(carContext.getString(commonR.string.close)) - .setOnClickListener { screenManager.pop() } - .build() - ) - .build() - } - val listLimit = carContext.getCarService(ConstraintManager::class.java) .getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST) - // Always reserve 2 rows for navigation (Previous + Next) to keep a fixed itemsPerPage - // across all pages, avoiding index drift when navigating back and forth. val itemsPerPage = (listLimit - 2).coerceAtLeast(1) val fromIndex = page * itemsPerPage diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index eacf68932d6..373b4ffbdd5 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1473,5 +1473,4 @@ No app available to open %s Experimental Learn more - " Not Allow on Driving" From b9f75c1fb7362bff7fa3b42a8df16b2eddcacbd2 Mon Sep 17 00:00:00 2001 From: cddu33 Date: Thu, 16 Apr 2026 00:21:51 +0200 Subject: [PATCH 03/49] improve detection car park --- .../companion/android/vehicle/BaseVehicleScreen.kt | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt index f83f6218434..c83e5a4382b 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt @@ -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 { From ceebc759e41907a6d045cd7b74ba9c0369507a10 Mon Sep 17 00:00:00 2001 From: cddu33 Date: Thu, 16 Apr 2026 13:46:21 +0200 Subject: [PATCH 04/49] correct favorite visible --- .../companion/android/vehicle/BaseVehicleScreen.kt | 6 ++++++ .../companion/android/vehicle/MainVehicleScreen.kt | 1 + .../android/vehicle/ManageFavoritesVehicleScreen.kt | 2 -- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt index c83e5a4382b..9d4ca239800 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt @@ -36,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) } carRestrictionManager?.registerListener(listener) + invalidate() } } } diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt index b59cfa0717b..c8c132c3029 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt @@ -218,6 +218,7 @@ class MainVehicleScreen( }.build() val headerBuilder = carContext.getHeaderBuilder(commonR.string.app_name, Action.APP_ICON) + Timber.d("onGetTemplate: isAutomotive=$isAutomotive isDrivingOptimized=$isDrivingOptimized") if (isAutomotive && !isDrivingOptimized) { if (BuildConfig.FLAVOR != "full") { headerBuilder.addEndHeaderAction(nativeModeAction(carContext)) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index f5455d67c3f..5475d3a31b4 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -70,9 +70,7 @@ class ManageFavoritesVehicleScreen( } override fun onDrivingOptimizedChanged(newState: Boolean) { - Timber.d("Etat conduite : $newState") if (newState) { - // On utilise le scope lié au cycle de vie pour revenir sur le thread UI lifecycleScope.launch { Timber.i("Fermeture de l'écran car la voiture roule") screenManager.pop() From 03b9cc0e163c457d6bafd843270585f4eb8db21d Mon Sep 17 00:00:00 2001 From: cddu33 Date: Thu, 16 Apr 2026 15:00:55 +0200 Subject: [PATCH 05/49] beta publication playstore --- app/src/beta/res/values/strings.xml | 4 ++++ automotive/build.gradle.kts | 5 +++++ automotive/src/main/AndroidManifest.xml | 1 - .../main/kotlin/AndroidApplicationConventionPlugin.kt | 9 +++++++++ 4 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 app/src/beta/res/values/strings.xml diff --git a/app/src/beta/res/values/strings.xml b/app/src/beta/res/values/strings.xml new file mode 100644 index 00000000000..a7936432da3 --- /dev/null +++ b/app/src/beta/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Ha Automotive Test + diff --git a/automotive/build.gradle.kts b/automotive/build.gradle.kts index 6b9b42ea274..2f08f4dd80e 100644 --- a/automotive/build.gradle.kts +++ b/automotive/build.gradle.kts @@ -71,6 +71,11 @@ android { directories += "../app/src/debug/res" } } + getByName("beta") { + res { + directories += "../app/src/beta/res" + } + } getByName("release") { kotlin { directories += "../app/src/release/kotlin" diff --git a/automotive/src/main/AndroidManifest.xml b/automotive/src/main/AndroidManifest.xml index 525da7da602..ce5cd719292 100644 --- a/automotive/src/main/AndroidManifest.xml +++ b/automotive/src/main/AndroidManifest.xml @@ -20,7 +20,6 @@ - diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt index 22e05e0488d..5039982b2d4 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -76,6 +76,15 @@ class AndroidApplicationConventionPlugin : Plugin { isJniDebuggable = false signingConfig = signingConfigs.getByName("release") } + create("beta") { + initWith(getByName("release")) + applicationIdSuffix = ".beta" + versionNameSuffix = "-beta" + isDebuggable = false + isJniDebuggable = false + signingConfig = signingConfigs.getByName("release") + matchingFallbacks += listOf("release") + } } } } From 251f8a2522ef988a0d8ad9e54df5f3b5427ae5bc Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:29:13 +0200 Subject: [PATCH 06/49] Update app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt Logging raw entityId can leak user-specific information (entity IDs often include user-defined names). Prefer removing these logs or wrapping the value with sensitive(...) as used elsewhere in the app. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../companion/android/vehicle/ManageFavoritesVehicleScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index 5475d3a31b4..e864a7e2ffe 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -127,10 +127,10 @@ class ManageFavoritesVehicleScreen( entityId = entity.entityId, ) if (isChecked) { - Timber.d("Adding favorite: ${entity.entityId}") + Timber.d("Adding favorite") prefsRepository.addAutoFavorite(favorite) } else { - Timber.d("Removing favorite: ${entity.entityId}") + Timber.d("Removing favorite") val updated = favoritesList.filterNot { it == favorite } prefsRepository.setAutoFavorites(updated) } From 74cc6f16f5b5cc066c7226f25d60bd615504b581 Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:29:50 +0200 Subject: [PATCH 07/49] Update common/src/main/res/values/strings.xml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit aa_manage_favorites_parked_only is added but not referenced anywhere in the PR. If it’s not needed, remove it to avoid unused resource warnings; if it is needed, wire it into the UI where the parked-only restriction is enforced. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- common/src/main/res/values/strings.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 373b4ffbdd5..ae91266609b 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1236,7 +1236,6 @@ Android Automotive Driving favorites Manage favorites - Managing favorites is only available when parked ← Previous page Next page → Alarm Control Panels From e2cd29743e841a3438f9c16646ba88a321d468a2 Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:31:07 +0200 Subject: [PATCH 08/49] Update app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt Kotlin style in this codebase generally uses trailing commas for multiline argument lists; add a trailing comma after prefsRepository to match surrounding calls and avoid ktlint churn. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../homeassistant/companion/android/vehicle/DomainListScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt index 52059a32c6f..0398dc2d53f 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt @@ -82,7 +82,7 @@ class DomainListScreen( screenManager, serverId, allEntities, - prefsRepository + prefsRepository, ), ) } From 82f214360710621fcee844057f8414879c4e2ded Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:31:44 +0200 Subject: [PATCH 09/49] Update app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt Kotlin style in this codebase generally uses trailing commas for multiline argument lists; add a trailing comma after prefsRepository to match surrounding calls and avoid ktlint churn. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../companion/android/vehicle/MainVehicleScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt index c8c132c3029..6002ec465cd 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt @@ -229,7 +229,7 @@ class MainVehicleScreen( screenManager, serverId, allEntities, - prefsRepository + prefsRepository, ), ) } From ce72bb61aedc24856ccce799b85607b4f460bc72 Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:32:26 +0200 Subject: [PATCH 10/49] Update app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sorting computes favorite membership with favoritesList.any { ... } for every entity, which is O(entities × favorites). Consider building a Set (or HashSet) of favorite entityIds for the current server once per update and using contains in both the sort and row-building logic. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../android/vehicle/ManageFavoritesVehicleScreen.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index e864a7e2ffe..4787d01c1fa 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -49,13 +49,18 @@ class ManageFavoritesVehicleScreen( lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { favoritesList = prefsRepository.getAutoFavorites() allEntities.collect { entityMap -> + val currentServerId = serverId.value + val favoriteEntityIds = favoritesList + .asSequence() + .filter { it.serverId == currentServerId } + .map { it.entityId } + .toSet() + val newEntities = entityMap.values .filter { it.domain in SUPPORTED_DOMAINS_WITH_STRING } .sortedWith( compareByDescending { entity -> - favoritesList.any { - it.serverId == serverId.value && it.entityId == entity.entityId - } + favoriteEntityIds.contains(entity.entityId) }.thenBy { it.attributes["friendly_name"]?.toString() ?: it.entityId }, ) if (newEntities.map { it.entityId } != entities.map { it.entityId }) { From 2bb7210bece2c826365843692bfd1408b32ddbfe Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:32:49 +0200 Subject: [PATCH 11/49] Update app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt itemsPerPage always reserves space for both paging rows by doing listLimit - 2, even when only one (or neither) paging row is shown. This reduces the number of entities shown per page unnecessarily and can make paging feel off. Consider calculating the reserved rows based on hasPreviousPage/hasNextPage for the current page. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../android/vehicle/ManageFavoritesVehicleScreen.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index 4787d01c1fa..86c9308576a 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -88,12 +88,14 @@ class ManageFavoritesVehicleScreen( 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 + val reservedRowsWithoutNextPage = if (hasPreviousPage) 1 else 0 + val maxItemsWithoutNextPage = (listLimit - reservedRowsWithoutNextPage).coerceAtLeast(1) + val fromIndex = page * maxItemsWithoutNextPage + val hasNextPage = fromIndex + maxItemsWithoutNextPage < entities.size + val reservedRows = reservedRowsWithoutNextPage + if (hasNextPage) 1 else 0 + val itemsPerPage = (listLimit - reservedRows).coerceAtLeast(1) + val toIndex = minOf(fromIndex + itemsPerPage, entities.size) val pageEntities = if (isLoaded) entities.subList(fromIndex, toIndex) else emptyList() val listBuilder = ItemList.Builder() From df516d2d913c50aae8c2476ce043518b09426897 Mon Sep 17 00:00:00 2001 From: cddu33 Date: Thu, 16 Apr 2026 18:38:03 +0200 Subject: [PATCH 12/49] clean logs --- .../companion/android/vehicle/BaseVehicleScreen.kt | 6 +----- .../companion/android/vehicle/MainVehicleScreen.kt | 1 - .../android/vehicle/ManageFavoritesVehicleScreen.kt | 1 - 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt index 9d4ca239800..e955c3b6bc1 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt @@ -36,18 +36,14 @@ 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") + Timber.d("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) } carRestrictionManager?.registerListener(listener) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt index 6002ec465cd..3ff706d5a9c 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt @@ -218,7 +218,6 @@ class MainVehicleScreen( }.build() val headerBuilder = carContext.getHeaderBuilder(commonR.string.app_name, Action.APP_ICON) - Timber.d("onGetTemplate: isAutomotive=$isAutomotive isDrivingOptimized=$isDrivingOptimized") if (isAutomotive && !isDrivingOptimized) { if (BuildConfig.FLAVOR != "full") { headerBuilder.addEndHeaderAction(nativeModeAction(carContext)) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index 86c9308576a..97570f8fdfe 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -77,7 +77,6 @@ class ManageFavoritesVehicleScreen( override fun onDrivingOptimizedChanged(newState: Boolean) { if (newState) { lifecycleScope.launch { - Timber.i("Fermeture de l'écran car la voiture roule") screenManager.pop() } } From d43b4aa58be3be4365441dd54f675ff73458b156 Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:40:45 +0200 Subject: [PATCH 13/49] Update .idea/markdown.xml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .idea/markdown.xml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.idea/markdown.xml b/.idea/markdown.xml index c61ea3346e8..e69de29bb2d 100644 --- a/.idea/markdown.xml +++ b/.idea/markdown.xml @@ -1,8 +0,0 @@ - - - - - - \ No newline at end of file From a473bc0b2117f0377be77b61cb8343f28c014670 Mon Sep 17 00:00:00 2001 From: cddu33 Date: Thu, 16 Apr 2026 18:41:27 +0200 Subject: [PATCH 14/49] clean logs --- .../homeassistant/companion/android/vehicle/BaseVehicleScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt index e955c3b6bc1..eb718ec2587 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt @@ -37,7 +37,6 @@ abstract class BaseVehicleScreen(carContext: CarContext) : Screen(carContext) { private fun registerAutomotiveRestrictionListener() { if (carContext.isAutomotive()) { - Timber.d("Register for Automotive Restrictions") car = Car.createCar(carContext) carRestrictionManager = car?.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE) as? CarUxRestrictionsManager From b65ce0153d60127f174ecc0593995e65d416b576 Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:51:23 +0200 Subject: [PATCH 15/49] Revert "beta publication playstore" (#2) This reverts commit 03b9cc0e163c457d6bafd843270585f4eb8db21d. Co-authored-by: Claude --- app/src/beta/res/values/strings.xml | 4 ---- automotive/build.gradle.kts | 5 ----- automotive/src/main/AndroidManifest.xml | 1 + .../main/kotlin/AndroidApplicationConventionPlugin.kt | 9 --------- 4 files changed, 1 insertion(+), 18 deletions(-) delete mode 100644 app/src/beta/res/values/strings.xml diff --git a/app/src/beta/res/values/strings.xml b/app/src/beta/res/values/strings.xml deleted file mode 100644 index a7936432da3..00000000000 --- a/app/src/beta/res/values/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - Ha Automotive Test - diff --git a/automotive/build.gradle.kts b/automotive/build.gradle.kts index 2f08f4dd80e..6b9b42ea274 100644 --- a/automotive/build.gradle.kts +++ b/automotive/build.gradle.kts @@ -71,11 +71,6 @@ android { directories += "../app/src/debug/res" } } - getByName("beta") { - res { - directories += "../app/src/beta/res" - } - } getByName("release") { kotlin { directories += "../app/src/release/kotlin" diff --git a/automotive/src/main/AndroidManifest.xml b/automotive/src/main/AndroidManifest.xml index ce5cd719292..525da7da602 100644 --- a/automotive/src/main/AndroidManifest.xml +++ b/automotive/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ + diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt index 5039982b2d4..22e05e0488d 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -76,15 +76,6 @@ class AndroidApplicationConventionPlugin : Plugin { isJniDebuggable = false signingConfig = signingConfigs.getByName("release") } - create("beta") { - initWith(getByName("release")) - applicationIdSuffix = ".beta" - versionNameSuffix = "-beta" - isDebuggable = false - isJniDebuggable = false - signingConfig = signingConfigs.getByName("release") - matchingFallbacks += listOf("release") - } } } } From ee5a15815fce3dae54422957dc5fee772f9236c5 Mon Sep 17 00:00:00 2001 From: cddu33 Date: Thu, 16 Apr 2026 19:08:19 +0200 Subject: [PATCH 16/49] clean logs --- .../companion/android/vehicle/ManageFavoritesVehicleScreen.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index 97570f8fdfe..4561fddca1e 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -133,10 +133,8 @@ class ManageFavoritesVehicleScreen( entityId = entity.entityId, ) if (isChecked) { - Timber.d("Adding favorite") prefsRepository.addAutoFavorite(favorite) } else { - Timber.d("Removing favorite") val updated = favoritesList.filterNot { it == favorite } prefsRepository.setAutoFavorites(updated) } From 622c2768b32928aa5451e726481d60809b82751b Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:08:51 +0200 Subject: [PATCH 17/49] Update app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt After toggling a favorite, favoritesList is updated but entities is not re-sorted. As a result, favorites may not move to the top until allEntities emits again, contradicting the intended behavior of keeping favorites sorted to the top. Recompute/re-sort entities (and potentially reset/clamp page) after updating favoritesList. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../android/vehicle/ManageFavoritesVehicleScreen.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index 4561fddca1e..0de1093ece4 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -139,6 +139,15 @@ class ManageFavoritesVehicleScreen( prefsRepository.setAutoFavorites(updated) } favoritesList = prefsRepository.getAutoFavorites() + val favoriteEntityIds = favoritesList + .filter { it.serverId == serverId.value } + .map { it.entityId } + .toSet() + entities = entities.sortedByDescending { updatedEntity -> + updatedEntity.entityId in favoriteEntityIds + } + val maxPage = if (entities.isEmpty()) 0 else (entities.size - 1) / listLimit + page = page.coerceIn(minimumValue = 0, maximumValue = maxPage) invalidate() } } From 6da3ba589def4222ca71f71f1b82937469d643ee Mon Sep 17 00:00:00 2001 From: cddu33 Date: Thu, 16 Apr 2026 19:09:58 +0200 Subject: [PATCH 18/49] delete unused val --- .../homeassistant/companion/android/vehicle/BaseVehicleScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt index eb718ec2587..cbfcd6ee84d 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt @@ -40,7 +40,6 @@ abstract class BaseVehicleScreen(carContext: CarContext) : Screen(carContext) { car = Car.createCar(carContext) carRestrictionManager = car?.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE) as? CarUxRestrictionsManager - val initialState = carRestrictionManager?.currentCarUxRestrictions?.isRequiresDistractionOptimization ?: false val listener = CarUxRestrictionsManager.OnUxRestrictionsChangedListener { restrictions -> onDrivingOptimizedChanged(restrictions.isRequiresDistractionOptimization) From 4af0bf076a97595b26d5061b17adf6dcf14fde21 Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:11:55 +0200 Subject: [PATCH 19/49] Copilot suggestion (#3) * Revert "beta publication playstore" (#2) This reverts commit 03b9cc0e163c457d6bafd843270585f4eb8db21d. Co-authored-by: Claude * clean logs * Update app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt After toggling a favorite, favoritesList is updated but entities is not re-sorted. As a result, favorites may not move to the top until allEntities emits again, contradicting the intended behavior of keeping favorites sorted to the top. Recompute/re-sort entities (and potentially reset/clamp page) after updating favoritesList. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * delete unused val --------- Co-authored-by: Claude Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/src/beta/res/values/strings.xml | 4 ---- .../companion/android/vehicle/BaseVehicleScreen.kt | 1 - .../android/vehicle/ManageFavoritesVehicleScreen.kt | 11 +++++++++-- automotive/build.gradle.kts | 5 ----- automotive/src/main/AndroidManifest.xml | 1 + .../main/kotlin/AndroidApplicationConventionPlugin.kt | 9 --------- 6 files changed, 10 insertions(+), 21 deletions(-) delete mode 100644 app/src/beta/res/values/strings.xml diff --git a/app/src/beta/res/values/strings.xml b/app/src/beta/res/values/strings.xml deleted file mode 100644 index a7936432da3..00000000000 --- a/app/src/beta/res/values/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - Ha Automotive Test - diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt index eb718ec2587..cbfcd6ee84d 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt @@ -40,7 +40,6 @@ abstract class BaseVehicleScreen(carContext: CarContext) : Screen(carContext) { car = Car.createCar(carContext) carRestrictionManager = car?.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE) as? CarUxRestrictionsManager - val initialState = carRestrictionManager?.currentCarUxRestrictions?.isRequiresDistractionOptimization ?: false val listener = CarUxRestrictionsManager.OnUxRestrictionsChangedListener { restrictions -> onDrivingOptimizedChanged(restrictions.isRequiresDistractionOptimization) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index 97570f8fdfe..0de1093ece4 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -133,14 +133,21 @@ class ManageFavoritesVehicleScreen( entityId = entity.entityId, ) if (isChecked) { - Timber.d("Adding favorite") prefsRepository.addAutoFavorite(favorite) } else { - Timber.d("Removing favorite") val updated = favoritesList.filterNot { it == favorite } prefsRepository.setAutoFavorites(updated) } favoritesList = prefsRepository.getAutoFavorites() + val favoriteEntityIds = favoritesList + .filter { it.serverId == serverId.value } + .map { it.entityId } + .toSet() + entities = entities.sortedByDescending { updatedEntity -> + updatedEntity.entityId in favoriteEntityIds + } + val maxPage = if (entities.isEmpty()) 0 else (entities.size - 1) / listLimit + page = page.coerceIn(minimumValue = 0, maximumValue = maxPage) invalidate() } } diff --git a/automotive/build.gradle.kts b/automotive/build.gradle.kts index 2f08f4dd80e..6b9b42ea274 100644 --- a/automotive/build.gradle.kts +++ b/automotive/build.gradle.kts @@ -71,11 +71,6 @@ android { directories += "../app/src/debug/res" } } - getByName("beta") { - res { - directories += "../app/src/beta/res" - } - } getByName("release") { kotlin { directories += "../app/src/release/kotlin" diff --git a/automotive/src/main/AndroidManifest.xml b/automotive/src/main/AndroidManifest.xml index ce5cd719292..525da7da602 100644 --- a/automotive/src/main/AndroidManifest.xml +++ b/automotive/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ + diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt index 5039982b2d4..22e05e0488d 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -76,15 +76,6 @@ class AndroidApplicationConventionPlugin : Plugin { isJniDebuggable = false signingConfig = signingConfigs.getByName("release") } - create("beta") { - initWith(getByName("release")) - applicationIdSuffix = ".beta" - versionNameSuffix = "-beta" - isDebuggable = false - isJniDebuggable = false - signingConfig = signingConfigs.getByName("release") - matchingFallbacks += listOf("release") - } } } } From 2df7f864aa4b0a40d1215c9398bea5734323ae05 Mon Sep 17 00:00:00 2001 From: cddu33 Date: Thu, 16 Apr 2026 19:33:38 +0200 Subject: [PATCH 20/49] revert beta --- app/src/beta/res/values/strings.xml | 4 ++++ automotive/build.gradle.kts | 5 +++++ automotive/src/main/AndroidManifest.xml | 1 - 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 app/src/beta/res/values/strings.xml diff --git a/app/src/beta/res/values/strings.xml b/app/src/beta/res/values/strings.xml new file mode 100644 index 00000000000..a7936432da3 --- /dev/null +++ b/app/src/beta/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Ha Automotive Test + diff --git a/automotive/build.gradle.kts b/automotive/build.gradle.kts index 6b9b42ea274..2f08f4dd80e 100644 --- a/automotive/build.gradle.kts +++ b/automotive/build.gradle.kts @@ -71,6 +71,11 @@ android { directories += "../app/src/debug/res" } } + getByName("beta") { + res { + directories += "../app/src/beta/res" + } + } getByName("release") { kotlin { directories += "../app/src/release/kotlin" diff --git a/automotive/src/main/AndroidManifest.xml b/automotive/src/main/AndroidManifest.xml index 525da7da602..ce5cd719292 100644 --- a/automotive/src/main/AndroidManifest.xml +++ b/automotive/src/main/AndroidManifest.xml @@ -20,7 +20,6 @@ - From 51bd2d3cd41cb901fc67af2c831580d15379401e Mon Sep 17 00:00:00 2001 From: cddu33 Date: Thu, 16 Apr 2026 19:47:12 +0200 Subject: [PATCH 21/49] revert beta --- .../main/kotlin/AndroidApplicationConventionPlugin.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt index 22e05e0488d..5039982b2d4 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -76,6 +76,15 @@ class AndroidApplicationConventionPlugin : Plugin { isJniDebuggable = false signingConfig = signingConfigs.getByName("release") } + create("beta") { + initWith(getByName("release")) + applicationIdSuffix = ".beta" + versionNameSuffix = "-beta" + isDebuggable = false + isJniDebuggable = false + signingConfig = signingConfigs.getByName("release") + matchingFallbacks += listOf("release") + } } } } From b413653ca715c8cf760d70c614bb13f649c3061c Mon Sep 17 00:00:00 2001 From: cddu33 Date: Thu, 16 Apr 2026 19:49:54 +0200 Subject: [PATCH 22/49] removed unused import --- .../companion/android/vehicle/ManageFavoritesVehicleScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index 0de1093ece4..9415c1ada22 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -21,7 +21,6 @@ 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 /** * A Car App screen that allows users to manage their automotive favorites when the vehicle is From 0ed7c4655f638bd15585fa1d0faf67949e948c3c Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:53:23 +0200 Subject: [PATCH 23/49] feature/add-favorite-automotive (#4) * Revert "beta publication playstore" (#2) This reverts commit 03b9cc0e163c457d6bafd843270585f4eb8db21d. Co-authored-by: Claude * clean logs * Update app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt After toggling a favorite, favoritesList is updated but entities is not re-sorted. As a result, favorites may not move to the top until allEntities emits again, contradicting the intended behavior of keeping favorites sorted to the top. Recompute/re-sort entities (and potentially reset/clamp page) after updating favoritesList. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * delete unused val * removed unused import --------- Co-authored-by: Claude Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/src/beta/res/values/strings.xml | 4 ---- .../android/vehicle/ManageFavoritesVehicleScreen.kt | 1 - automotive/build.gradle.kts | 5 ----- automotive/src/main/AndroidManifest.xml | 1 + .../main/kotlin/AndroidApplicationConventionPlugin.kt | 9 --------- 5 files changed, 1 insertion(+), 19 deletions(-) delete mode 100644 app/src/beta/res/values/strings.xml diff --git a/app/src/beta/res/values/strings.xml b/app/src/beta/res/values/strings.xml deleted file mode 100644 index a7936432da3..00000000000 --- a/app/src/beta/res/values/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - Ha Automotive Test - diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index 0de1093ece4..9415c1ada22 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -21,7 +21,6 @@ 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 /** * A Car App screen that allows users to manage their automotive favorites when the vehicle is diff --git a/automotive/build.gradle.kts b/automotive/build.gradle.kts index 2f08f4dd80e..6b9b42ea274 100644 --- a/automotive/build.gradle.kts +++ b/automotive/build.gradle.kts @@ -71,11 +71,6 @@ android { directories += "../app/src/debug/res" } } - getByName("beta") { - res { - directories += "../app/src/beta/res" - } - } getByName("release") { kotlin { directories += "../app/src/release/kotlin" diff --git a/automotive/src/main/AndroidManifest.xml b/automotive/src/main/AndroidManifest.xml index ce5cd719292..525da7da602 100644 --- a/automotive/src/main/AndroidManifest.xml +++ b/automotive/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ + diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt index 5039982b2d4..22e05e0488d 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -76,15 +76,6 @@ class AndroidApplicationConventionPlugin : Plugin { isJniDebuggable = false signingConfig = signingConfigs.getByName("release") } - create("beta") { - initWith(getByName("release")) - applicationIdSuffix = ".beta" - versionNameSuffix = "-beta" - isDebuggable = false - isJniDebuggable = false - signingConfig = signingConfigs.getByName("release") - matchingFallbacks += listOf("release") - } } } } From 3b443959967eb62262684bdbd17877b8b50602f3 Mon Sep 17 00:00:00 2001 From: cddu33 Date: Thu, 16 Apr 2026 19:59:25 +0200 Subject: [PATCH 24/49] clean import --- .../homeassistant/companion/android/vehicle/BaseVehicleScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt index cbfcd6ee84d..305b958cd96 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt @@ -7,7 +7,6 @@ import androidx.car.app.Screen import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import io.homeassistant.companion.android.common.util.isAutomotive -import timber.log.Timber abstract class BaseVehicleScreen(carContext: CarContext) : Screen(carContext) { private var car: Car? = null From 0a7425921df480f3f8ff441268569f5c720ac062 Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 16 Apr 2026 20:13:39 +0200 Subject: [PATCH 25/49] Feature/add favorite automotive (#5) * Revert "beta publication playstore" (#2) This reverts commit 03b9cc0e163c457d6bafd843270585f4eb8db21d. Co-authored-by: Claude * clean logs * Update app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt After toggling a favorite, favoritesList is updated but entities is not re-sorted. As a result, favorites may not move to the top until allEntities emits again, contradicting the intended behavior of keeping favorites sorted to the top. Recompute/re-sort entities (and potentially reset/clamp page) after updating favoritesList. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * delete unused val * removed unused import * clean import --------- Co-authored-by: Claude Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../homeassistant/companion/android/vehicle/BaseVehicleScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt index cbfcd6ee84d..305b958cd96 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt @@ -7,7 +7,6 @@ import androidx.car.app.Screen import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import io.homeassistant.companion.android.common.util.isAutomotive -import timber.log.Timber abstract class BaseVehicleScreen(carContext: CarContext) : Screen(carContext) { private var car: Car? = null From 2782057a40f1da1c89421fa8b22547c2f77793b8 Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 16 Apr 2026 20:21:52 +0200 Subject: [PATCH 26/49] Reorder imports in DomainListScreen.kt --- .../companion/android/vehicle/DomainListScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt index 0398dc2d53f..d1ba97ad58f 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt @@ -13,11 +13,11 @@ import io.homeassistant.companion.android.common.data.prefs.PrefsRepository import io.homeassistant.companion.android.common.data.servers.ServerManager import io.homeassistant.companion.android.common.data.websocket.impl.entities.EntityRegistryResponse import io.homeassistant.companion.android.common.util.isAutomotive -import io.homeassistant.companion.android.util.vehicle.SUPPORTED_DOMAINS import io.homeassistant.companion.android.util.vehicle.getDomainList +import io.homeassistant.companion.android.util.vehicle.getManageFavoritesAction 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 io.homeassistant.companion.android.util.vehicle.SUPPORTED_DOMAINS import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch From 9c2ca7371cb54c0b8878e699792be583481554f1 Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 16 Apr 2026 20:30:00 +0200 Subject: [PATCH 27/49] Update DomainListScreen.kt --- .../companion/android/vehicle/DomainListScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt index d1ba97ad58f..87595627e6a 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt @@ -13,11 +13,11 @@ import io.homeassistant.companion.android.common.data.prefs.PrefsRepository import io.homeassistant.companion.android.common.data.servers.ServerManager import io.homeassistant.companion.android.common.data.websocket.impl.entities.EntityRegistryResponse import io.homeassistant.companion.android.common.util.isAutomotive +import io.homeassistant.companion.android.util.vehicle.SUPPORTED_DOMAINS import io.homeassistant.companion.android.util.vehicle.getDomainList -import io.homeassistant.companion.android.util.vehicle.getManageFavoritesAction import io.homeassistant.companion.android.util.vehicle.getHeaderBuilder +import io.homeassistant.companion.android.util.vehicle.getManageFavoritesAction import io.homeassistant.companion.android.util.vehicle.nativeModeAction -import io.homeassistant.companion.android.util.vehicle.SUPPORTED_DOMAINS import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch From d9e331b6f7128ba55e0f862398e03be5330330cd Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 16 Apr 2026 21:20:56 +0200 Subject: [PATCH 28/49] clean @API --- .../companion/android/util/vehicle/TemplateComponents.kt | 1 - .../companion/android/vehicle/ManageFavoritesVehicleScreen.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt b/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt index 6ceb8c7716f..5a4f1ecf12f 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt @@ -89,7 +89,6 @@ fun getChangeServerGridItem( } } -@RequiresApi(Build.VERSION_CODES.O) fun getNavigationGridItem( carContext: CarContext, screenManager: ScreenManager, diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index 9415c1ada22..4b418a96e76 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -30,7 +30,6 @@ import kotlinx.coroutines.launch * This screen stays fully within the Car App API, making it compliant with Play Store * automotive distribution policies. */ -@RequiresApi(Build.VERSION_CODES.O) class ManageFavoritesVehicleScreen( carContext: CarContext, private val serverId: StateFlow, From 80bcfc0c5f178ae6695776bcccbe231fa3ffbb74 Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 16 Apr 2026 21:22:48 +0200 Subject: [PATCH 29/49] clean @api --- .../companion/android/util/vehicle/TemplateComponents.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt b/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt index 5a4f1ecf12f..4a69501d09f 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt @@ -131,7 +131,6 @@ fun getNavigationGridItem( } } -@RequiresApi(Build.VERSION_CODES.O) fun getDomainList( domains: MutableSet, carContext: CarContext, From 10f6bf9ef54835154874f99b2646c501c1ed4ab5 Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 16 Apr 2026 21:23:06 +0200 Subject: [PATCH 30/49] clean @api --- .../companion/android/util/vehicle/TemplateComponents.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt b/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt index 4a69501d09f..2974597d7fe 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt @@ -216,7 +216,6 @@ fun getDomainList( return listBuilder } -@RequiresApi(Build.VERSION_CODES.O) fun getDomainsGridItem( carContext: CarContext, screenManager: ScreenManager, @@ -261,7 +260,6 @@ fun getDomainsGridItem( * 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) fun getManageFavoritesAction( carContext: CarContext, screenManager: ScreenManager, From b622996245606f7a9bea307342e2c1c30f181542 Mon Sep 17 00:00:00 2001 From: cddu33 Date: Thu, 16 Apr 2026 21:26:07 +0200 Subject: [PATCH 31/49] beta --- .../src/main/kotlin/AndroidApplicationConventionPlugin.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt index 22e05e0488d..59942ea2225 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -76,6 +76,11 @@ class AndroidApplicationConventionPlugin : Plugin { isJniDebuggable = false signingConfig = signingConfigs.getByName("release") } + create("beta") { + initWith(getByName("release")) + matchingFallbacks += listOf("release") + applicationIdSuffix = ".beta" + } } } } From dea98cae275538b7aa8a404c7272b76ff534ccf7 Mon Sep 17 00:00:00 2001 From: cddu33 Date: Thu, 16 Apr 2026 21:45:45 +0200 Subject: [PATCH 32/49] beta --- .github/actions/inflate-secrets/action.yml | 3 +++ .github/workflows/onPush.yml | 4 ++-- automotive/build.gradle.kts | 5 +++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/actions/inflate-secrets/action.yml b/.github/actions/inflate-secrets/action.yml index 01c7d1c75e5..8a30e5d8c9d 100644 --- a/.github/actions/inflate-secrets/action.yml +++ b/.github/actions/inflate-secrets/action.yml @@ -21,6 +21,9 @@ runs: run: | cp .github/mock-google-services.json app/src/debug/google-services.json cp .github/mock-google-services.json app/src/minimal/google-services.json + mkdir -p app/src/beta automotive/src/beta + cp .github/mock-google-services.json app/src/beta/google-services.json + cp .github/mock-google-services.json automotive/src/beta/google-services.json - name: Inflate release_keystore.keystore shell: bash diff --git a/.github/workflows/onPush.yml b/.github/workflows/onPush.yml index 0f0219826ff..3700a5d02a8 100644 --- a/.github/workflows/onPush.yml +++ b/.github/workflows/onPush.yml @@ -67,7 +67,7 @@ jobs: VERSION_CODE: ${{ steps.rel_number.outputs.version-code }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} run: | - ./gradlew :common:assemble :app:assembleRelease :wear:assembleRelease :automotive:assembleRelease + ./gradlew :common:assemble :app:assembleRelease :app:assembleFullBeta :wear:assembleRelease :automotive:assembleRelease :automotive:assembleFullBeta - name: Archive Build uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -188,7 +188,7 @@ jobs: VERSION_CODE: ${{ steps.rel_number.outputs.version-code }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} run: | - ./gradlew :common:assemble :app:bundleFullRelease :wear:bundleRelease :automotive:bundleFullRelease + ./gradlew :common:assemble :app:bundleFullRelease :app:bundleFullBeta :wear:bundleRelease :automotive:bundleFullRelease :automotive:bundleFullBeta - name: Archive Build uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/automotive/build.gradle.kts b/automotive/build.gradle.kts index 6b9b42ea274..4cce4377faa 100644 --- a/automotive/build.gradle.kts +++ b/automotive/build.gradle.kts @@ -63,6 +63,11 @@ android { directories += "../app/src/minimal/res" } } + getByName("beta") { + res { + directories += "../app/src/beta/res" + } + } getByName("debug") { kotlin { directories += "../app/src/debug/kotlin" From 9bab495e8fa266e0282a6000d7872353273354d1 Mon Sep 17 00:00:00 2001 From: Clement DIBOUT Date: Mon, 20 Apr 2026 17:45:29 +0200 Subject: [PATCH 33/49] Revert "beta" This reverts commit dea98cae275538b7aa8a404c7272b76ff534ccf7. --- .github/actions/inflate-secrets/action.yml | 3 --- .github/workflows/onPush.yml | 4 ++-- automotive/build.gradle.kts | 5 ----- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/actions/inflate-secrets/action.yml b/.github/actions/inflate-secrets/action.yml index 8a30e5d8c9d..01c7d1c75e5 100644 --- a/.github/actions/inflate-secrets/action.yml +++ b/.github/actions/inflate-secrets/action.yml @@ -21,9 +21,6 @@ runs: run: | cp .github/mock-google-services.json app/src/debug/google-services.json cp .github/mock-google-services.json app/src/minimal/google-services.json - mkdir -p app/src/beta automotive/src/beta - cp .github/mock-google-services.json app/src/beta/google-services.json - cp .github/mock-google-services.json automotive/src/beta/google-services.json - name: Inflate release_keystore.keystore shell: bash diff --git a/.github/workflows/onPush.yml b/.github/workflows/onPush.yml index 3700a5d02a8..0f0219826ff 100644 --- a/.github/workflows/onPush.yml +++ b/.github/workflows/onPush.yml @@ -67,7 +67,7 @@ jobs: VERSION_CODE: ${{ steps.rel_number.outputs.version-code }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} run: | - ./gradlew :common:assemble :app:assembleRelease :app:assembleFullBeta :wear:assembleRelease :automotive:assembleRelease :automotive:assembleFullBeta + ./gradlew :common:assemble :app:assembleRelease :wear:assembleRelease :automotive:assembleRelease - name: Archive Build uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -188,7 +188,7 @@ jobs: VERSION_CODE: ${{ steps.rel_number.outputs.version-code }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} run: | - ./gradlew :common:assemble :app:bundleFullRelease :app:bundleFullBeta :wear:bundleRelease :automotive:bundleFullRelease :automotive:bundleFullBeta + ./gradlew :common:assemble :app:bundleFullRelease :wear:bundleRelease :automotive:bundleFullRelease - name: Archive Build uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/automotive/build.gradle.kts b/automotive/build.gradle.kts index 4cce4377faa..6b9b42ea274 100644 --- a/automotive/build.gradle.kts +++ b/automotive/build.gradle.kts @@ -63,11 +63,6 @@ android { directories += "../app/src/minimal/res" } } - getByName("beta") { - res { - directories += "../app/src/beta/res" - } - } getByName("debug") { kotlin { directories += "../app/src/debug/kotlin" From 00c78795e02ea74ce8d310262be78b54b6ec26e9 Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Mon, 20 Apr 2026 18:11:36 +0200 Subject: [PATCH 34/49] remove unused import --- .../companion/android/util/vehicle/TemplateComponents.kt | 2 -- .../companion/android/vehicle/ManageFavoritesVehicleScreen.kt | 2 -- 2 files changed, 4 deletions(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt b/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt index 2974597d7fe..df944ea580d 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt @@ -1,7 +1,5 @@ package io.homeassistant.companion.android.util.vehicle -import android.os.Build -import androidx.annotation.RequiresApi import androidx.annotation.StringRes import androidx.car.app.CarContext import androidx.car.app.ScreenManager diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index 4b418a96e76..6936bd8a8b9 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -1,7 +1,5 @@ package io.homeassistant.companion.android.vehicle -import android.os.Build -import androidx.annotation.RequiresApi import androidx.car.app.CarContext import androidx.car.app.constraints.ConstraintManager import androidx.car.app.model.ItemList From 86b234ed5a88ab8c5cee4eadf78d2633278651d8 Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Mon, 20 Apr 2026 18:54:14 +0200 Subject: [PATCH 35/49] correct src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt:114: Error: Call requires API level 26 (current min is 23): MapVehicleScreen [NewApi] --- .../companion/android/util/vehicle/TemplateComponents.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt b/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt index df944ea580d..e66dbb12a51 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt @@ -1,5 +1,7 @@ package io.homeassistant.companion.android.util.vehicle +import android.os.Build +import androidx.annotation.RequiresApi import androidx.annotation.StringRes import androidx.car.app.CarContext import androidx.car.app.ScreenManager @@ -87,6 +89,7 @@ fun getChangeServerGridItem( } } +@RequiresApi(Build.VERSION_CODES.O) fun getNavigationGridItem( carContext: CarContext, screenManager: ScreenManager, From d58dba4bad12d88c1c312b9282fbc9e27354b8d9 Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 23 Apr 2026 14:19:55 +0200 Subject: [PATCH 36/49] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../android/vehicle/ManageFavoritesVehicleScreen.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index 6936bd8a8b9..260a2e0c5ca 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -91,7 +91,11 @@ class ManageFavoritesVehicleScreen( val reservedRows = reservedRowsWithoutNextPage + if (hasNextPage) 1 else 0 val itemsPerPage = (listLimit - reservedRows).coerceAtLeast(1) val toIndex = minOf(fromIndex + itemsPerPage, entities.size) - val pageEntities = if (isLoaded) entities.subList(fromIndex, toIndex) else emptyList() + val pageEntities = if (isLoaded && fromIndex < entities.size) { + entities.subList(fromIndex, toIndex) + } else { + emptyList() + } val listBuilder = ItemList.Builder() From edb26259a0ecd1e141b716671fdc7c5822b5a62f Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Tue, 5 May 2026 13:04:45 +0200 Subject: [PATCH 37/49] delete markdown.xml --- .idea/markdown.xml | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .idea/markdown.xml diff --git a/.idea/markdown.xml b/.idea/markdown.xml deleted file mode 100644 index e69de29bb2d..00000000000 From 9358e49d07b56e330d0ae5959916c46ee43db418 Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Tue, 5 May 2026 13:14:26 +0200 Subject: [PATCH 38/49] protec isDrivingOptimized --- .../companion/android/vehicle/BaseVehicleScreen.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt index 305b958cd96..259268b3cf2 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt @@ -11,11 +11,16 @@ import io.homeassistant.companion.android.common.util.isAutomotive abstract class BaseVehicleScreen(carContext: CarContext) : Screen(carContext) { private var car: Car? = null private var carRestrictionManager: CarUxRestrictionsManager? = null - protected val isDrivingOptimized - get() = carRestrictionManager + protected val isDrivingOptimized: Boolean + get() = try { + carRestrictionManager ?.currentCarUxRestrictions ?.isRequiresDistractionOptimization ?: false + } catch (e: Exception) { + Timber.e(e, "Failed to get driving optimization state") + false + } init { lifecycle.addObserver(object : DefaultLifecycleObserver { From db572010fb7f4a10b0ec0b504c075ee708d8ee04 Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Tue, 5 May 2026 13:18:34 +0200 Subject: [PATCH 39/49] btn favorite on all automotive --- .../companion/android/vehicle/DomainListScreen.kt | 8 +++++--- .../companion/android/vehicle/MainVehicleScreen.kt | 7 ++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt index 87595627e6a..b709807a1f3 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt @@ -75,8 +75,9 @@ class DomainListScreen( if (isAutomotive && !isDrivingOptimized) { if (BuildConfig.FLAVOR != "full") { headerBuilder.addEndHeaderAction(nativeModeAction(carContext)) - } else { - headerBuilder.addEndHeaderAction( + } + + headerBuilder.addEndHeaderAction( getManageFavoritesAction( carContext, screenManager, @@ -85,7 +86,8 @@ class DomainListScreen( prefsRepository, ), ) - } + + } setHeader(headerBuilder.build()) val domainBuild = domainList.build() diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt index 3ff706d5a9c..d697830f415 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt @@ -221,8 +221,8 @@ class MainVehicleScreen( if (isAutomotive && !isDrivingOptimized) { if (BuildConfig.FLAVOR != "full") { headerBuilder.addEndHeaderAction(nativeModeAction(carContext)) - } else { - headerBuilder.addEndHeaderAction( + } + headerBuilder.addEndHeaderAction( getManageFavoritesAction( carContext, screenManager, @@ -231,7 +231,8 @@ class MainVehicleScreen( prefsRepository, ), ) - } + + } headerBuilder.addEndHeaderAction(refreshAction) From 549a9ee32541ac427fef293b9b83c3c48b8d4e9e Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Tue, 5 May 2026 13:27:39 +0200 Subject: [PATCH 40/49] delete icon Previous/Next page --- common/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index ff227d2ed83..a30fa1413c2 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1238,8 +1238,8 @@ Android Automotive Driving favorites Manage favorites - ← Previous page - Next page → + Previous page + Next page Alarm Control Panels Triggered Disarmed From bcabf58749672fed485d712d9991cc5039a6f86c Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Tue, 5 May 2026 13:31:04 +0200 Subject: [PATCH 41/49] correct manageFavorite --- .../vehicle/ManageFavoritesVehicleScreen.kt | 190 ++++++++++-------- 1 file changed, 109 insertions(+), 81 deletions(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index 260a2e0c5ca..309c7d0f663 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -19,6 +19,8 @@ import io.homeassistant.companion.android.util.vehicle.getHeaderBuilder import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock /** * A Car App screen that allows users to manage their automotive favorites when the vehicle is @@ -39,16 +41,16 @@ class ManageFavoritesVehicleScreen( private var favoritesList: List = emptyList() private var isLoaded = false private var page = 0 + private val toggleMutex = Mutex() init { lifecycleScope.launch { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { favoritesList = prefsRepository.getAutoFavorites() allEntities.collect { entityMap -> - val currentServerId = serverId.value val favoriteEntityIds = favoritesList .asSequence() - .filter { it.serverId == currentServerId } + .filter { it.serverId == serverId.value } .map { it.entityId } .toSet() @@ -59,12 +61,12 @@ class ManageFavoritesVehicleScreen( favoriteEntityIds.contains(entity.entityId) }.thenBy { it.attributes["friendly_name"]?.toString() ?: it.entityId }, ) - if (newEntities.map { it.entityId } != entities.map { it.entityId }) { - page = 0 - } + val listChanged = newEntities.map { it.entityId } != entities.map { it.entityId } + if (listChanged) page = 0 + val shouldInvalidate = !isLoaded || listChanged entities = newEntities isLoaded = true - invalidate() + if (shouldInvalidate) invalidate() } } } @@ -82,91 +84,44 @@ class ManageFavoritesVehicleScreen( override fun onGetTemplate(): Template { val listLimit = carContext.getCarService(ConstraintManager::class.java) .getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST) - - val hasPreviousPage = page > 0 - val reservedRowsWithoutNextPage = if (hasPreviousPage) 1 else 0 - val maxItemsWithoutNextPage = (listLimit - reservedRowsWithoutNextPage).coerceAtLeast(1) - val fromIndex = page * maxItemsWithoutNextPage - val hasNextPage = fromIndex + maxItemsWithoutNextPage < entities.size - val reservedRows = reservedRowsWithoutNextPage + if (hasNextPage) 1 else 0 - val itemsPerPage = (listLimit - reservedRows).coerceAtLeast(1) - val toIndex = minOf(fromIndex + itemsPerPage, entities.size) - val pageEntities = if (isLoaded && fromIndex < entities.size) { - entities.subList(fromIndex, toIndex) + val pageSlice = computePageSlice(entities.size, page, listLimit) + val pageEntities = if (isLoaded && pageSlice.fromIndex < entities.size) { + entities.subList(pageSlice.fromIndex, pageSlice.toIndex) } else { emptyList() } + return ListTemplate.Builder() + .setHeader(carContext.getHeaderBuilder(commonR.string.android_automotive_favorites).build()) + .setLoading(!isLoaded) + .apply { + if (isLoaded) setSingleList(buildList(pageEntities, pageSlice).build()) + } + .build() + } + + private fun buildList(pageEntities: List, pageSlice: PageSlice): ItemList.Builder { val listBuilder = ItemList.Builder() - if (hasPreviousPage) { + if (pageSlice.hasPreviousPage) { listBuilder.addItem( - Row.Builder() - .setTitle(carContext.getString(commonR.string.aa_previous_page)) - .setOnClickListener { - page-- - invalidate() - } - .build(), + buildNavigationRow(commonR.string.aa_previous_page) { + page-- + invalidate() + }, ) } 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) { - prefsRepository.addAutoFavorite(favorite) - } else { - val updated = favoritesList.filterNot { it == favorite } - prefsRepository.setAutoFavorites(updated) - } - favoritesList = prefsRepository.getAutoFavorites() - val favoriteEntityIds = favoritesList - .filter { it.serverId == serverId.value } - .map { it.entityId } - .toSet() - entities = entities.sortedByDescending { updatedEntity -> - updatedEntity.entityId in favoriteEntityIds - } - val maxPage = if (entities.isEmpty()) 0 else (entities.size - 1) / listLimit - page = page.coerceIn(minimumValue = 0, maximumValue = maxPage) - invalidate() - } - } - .setChecked(isFavorite) - .build(), - ) - .build(), - ) + listBuilder.addItem(buildEntityRow(entity)) } - if (hasNextPage) { + if (pageSlice.hasNextPage) { listBuilder.addItem( - Row.Builder() - .setTitle(carContext.getString(commonR.string.aa_next_page)) - .setOnClickListener { - page++ - invalidate() - } - .build(), + buildNavigationRow(commonR.string.aa_next_page) { + page++ + invalidate() + }, ) } @@ -174,10 +129,83 @@ class ManageFavoritesVehicleScreen( 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()) } + return listBuilder + } + + private fun buildNavigationRow(titleRes: Int, onClick: () -> Unit): Row = + Row.Builder() + .setTitle(carContext.getString(titleRes)) + .setOnClickListener(onClick) + .build() + + private fun buildEntityRow(entity: Entity): Row { + 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 + + return Row.Builder() + .setTitle(friendlyName) + .addText(domainLabel) + .setToggle( + Toggle.Builder { isChecked -> + lifecycleScope.launch { + toggleMutex.withLock { + val favorite = AutoFavorite( + serverId = serverId.value, + entityId = entity.entityId, + ) + if (isChecked) { + prefsRepository.addAutoFavorite(favorite) + } else { + prefsRepository.setAutoFavorites( + favoritesList.filterNot { it == favorite }, + ) + } + favoritesList = prefsRepository.getAutoFavorites() + val favoriteEntityIds = favoritesList + .filter { it.serverId == serverId.value } + .map { it.entityId } + .toSet() + entities = entities.sortedWith( + compareByDescending { it.entityId in favoriteEntityIds } + .thenBy { it.attributes["friendly_name"]?.toString() ?: it.entityId }, + ) + invalidate() + } + } + } + .setChecked(isFavorite) + .build(), + ) .build() } } + +internal data class PageSlice( + val fromIndex: Int, + val toIndex: Int, + val hasPreviousPage: Boolean, + val hasNextPage: Boolean, +) + +/** + * Computes the slice of entities to display for the given page. + * + * Always reserves 2 rows for navigation (previous/next), giving a consistent + * [itemsPerPage] across all pages and avoiding skipped entities. + */ +internal fun computePageSlice(totalItems: Int, page: Int, listLimit: Int): PageSlice { + val itemsPerPage = (listLimit - 2).coerceAtLeast(1) + val fromIndex = page * itemsPerPage + val toIndex = minOf(fromIndex + itemsPerPage, totalItems) + return PageSlice( + fromIndex = fromIndex, + toIndex = toIndex, + hasPreviousPage = page > 0, + hasNextPage = toIndex < totalItems, + ) +} \ No newline at end of file From a038ae66831f1bcb466acab8bbb31ce44f509b0d Mon Sep 17 00:00:00 2001 From: cddu33 Date: Tue, 5 May 2026 21:09:58 +0200 Subject: [PATCH 42/49] remove timber log --- .../homeassistant/companion/android/vehicle/BaseVehicleScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt index 259268b3cf2..0e5e745e3d3 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt @@ -18,7 +18,6 @@ abstract class BaseVehicleScreen(carContext: CarContext) : Screen(carContext) { ?.isRequiresDistractionOptimization ?: false } catch (e: Exception) { - Timber.e(e, "Failed to get driving optimization state") false } From 1917abfe8ae2920038bea95685727e611860ae53 Mon Sep 17 00:00:00 2001 From: cddu33 Date: Tue, 5 May 2026 22:06:00 +0200 Subject: [PATCH 43/49] clean space and line --- .../android/vehicle/BaseVehicleScreen.kt | 16 +++++++-------- .../android/vehicle/DomainListScreen.kt | 20 +++++++++---------- .../android/vehicle/MainVehicleScreen.kt | 18 ++++++++--------- .../vehicle/ManageFavoritesVehicleScreen.kt | 2 +- 4 files changed, 26 insertions(+), 30 deletions(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt index 0e5e745e3d3..1a658d22c49 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/BaseVehicleScreen.kt @@ -12,14 +12,14 @@ abstract class BaseVehicleScreen(carContext: CarContext) : Screen(carContext) { private var car: Car? = null private var carRestrictionManager: CarUxRestrictionsManager? = null protected val isDrivingOptimized: Boolean - get() = try { - carRestrictionManager - ?.currentCarUxRestrictions - ?.isRequiresDistractionOptimization - ?: false - } catch (e: Exception) { - false - } + get() = try { + carRestrictionManager + ?.currentCarUxRestrictions + ?.isRequiresDistractionOptimization + ?: false + } catch (e: Exception) { + false + } init { lifecycle.addObserver(object : DefaultLifecycleObserver { diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt index b709807a1f3..3a673ee754e 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt @@ -75,19 +75,17 @@ class DomainListScreen( if (isAutomotive && !isDrivingOptimized) { if (BuildConfig.FLAVOR != "full") { headerBuilder.addEndHeaderAction(nativeModeAction(carContext)) - } + } headerBuilder.addEndHeaderAction( - getManageFavoritesAction( - carContext, - screenManager, - serverId, - allEntities, - prefsRepository, - ), - ) - - + getManageFavoritesAction( + carContext, + screenManager, + serverId, + allEntities, + prefsRepository, + ), + ) } setHeader(headerBuilder.build()) val domainBuild = domainList.build() diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt index d697830f415..19be28f498a 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt @@ -223,16 +223,14 @@ class MainVehicleScreen( headerBuilder.addEndHeaderAction(nativeModeAction(carContext)) } headerBuilder.addEndHeaderAction( - getManageFavoritesAction( - carContext, - screenManager, - serverId, - allEntities, - prefsRepository, - ), - ) - - + getManageFavoritesAction( + carContext, + screenManager, + serverId, + allEntities, + prefsRepository, + ), + ) } headerBuilder.addEndHeaderAction(refreshAction) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index 309c7d0f663..bf7375e5b9a 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -208,4 +208,4 @@ internal fun computePageSlice(totalItems: Int, page: Int, listLimit: Int): PageS hasPreviousPage = page > 0, hasNextPage = toIndex < totalItems, ) -} \ No newline at end of file +} From a4007d5b166b61c9aa775df67ae732ee2c402ab0 Mon Sep 17 00:00:00 2001 From: cddu33 Date: Tue, 5 May 2026 22:34:31 +0200 Subject: [PATCH 44/49] clean space and line --- .../companion/android/vehicle/DomainListScreen.kt | 2 +- .../companion/android/vehicle/MainVehicleScreen.kt | 2 +- .../android/vehicle/ManageFavoritesVehicleScreen.kt | 9 ++++----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt index 3a673ee754e..029df048e26 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/DomainListScreen.kt @@ -85,7 +85,7 @@ class DomainListScreen( allEntities, prefsRepository, ), - ) + ) } setHeader(headerBuilder.build()) val domainBuild = domainList.build() diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt index 19be28f498a..49fb0b59f5d 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/MainVehicleScreen.kt @@ -230,7 +230,7 @@ class MainVehicleScreen( allEntities, prefsRepository, ), - ) + ) } headerBuilder.addEndHeaderAction(refreshAction) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index bf7375e5b9a..586aaf4897a 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -132,11 +132,10 @@ class ManageFavoritesVehicleScreen( return listBuilder } - private fun buildNavigationRow(titleRes: Int, onClick: () -> Unit): Row = - Row.Builder() - .setTitle(carContext.getString(titleRes)) - .setOnClickListener(onClick) - .build() + private fun buildNavigationRow(titleRes: Int, onClick: () -> Unit): Row = Row.Builder() + .setTitle(carContext.getString(titleRes)) + .setOnClickListener(onClick) + .build() private fun buildEntityRow(entity: Entity): Row { val isFavorite = favoritesList.any { From 54be43f26357a22446a4aadc6a0b3b1ac929d950 Mon Sep 17 00:00:00 2001 From: cddu33 Date: Tue, 5 May 2026 23:14:26 +0200 Subject: [PATCH 45/49] api 26/23 @RequiresApi(Build.VERSION_CODES.O) --- .../companion/android/util/vehicle/TemplateComponents.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt b/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt index e66dbb12a51..bc22b0bf2c6 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/TemplateComponents.kt @@ -132,6 +132,7 @@ fun getNavigationGridItem( } } +@RequiresApi(Build.VERSION_CODES.O) fun getDomainList( domains: MutableSet, carContext: CarContext, @@ -217,6 +218,7 @@ fun getDomainList( return listBuilder } +@RequiresApi(Build.VERSION_CODES.O) fun getDomainsGridItem( carContext: CarContext, screenManager: ScreenManager, From b8729b273d758402237258e923fc3e3079274c9d Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 7 May 2026 10:09:03 +0200 Subject: [PATCH 46/49] Update app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt Co-authored-by: Timothy <6560631+TimoPtr@users.noreply.github.com> --- .../companion/android/vehicle/ManageFavoritesVehicleScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index 586aaf4897a..399b1747b0e 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -59,7 +59,7 @@ class ManageFavoritesVehicleScreen( .sortedWith( compareByDescending { entity -> favoriteEntityIds.contains(entity.entityId) - }.thenBy { it.attributes["friendly_name"]?.toString() ?: it.entityId }, + }.thenBy { it.friendlyName }, ) val listChanged = newEntities.map { it.entityId } != entities.map { it.entityId } if (listChanged) page = 0 From 110f7fa154b6a7e220b2f51d7876439f806b5797 Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 7 May 2026 10:09:16 +0200 Subject: [PATCH 47/49] Update app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt Co-authored-by: Timothy <6560631+TimoPtr@users.noreply.github.com> --- .../companion/android/vehicle/ManageFavoritesVehicleScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index 399b1747b0e..4f4b30b04f8 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -141,7 +141,7 @@ class ManageFavoritesVehicleScreen( val isFavorite = favoritesList.any { it.serverId == serverId.value && it.entityId == entity.entityId } - val friendlyName = entity.attributes["friendly_name"]?.toString() ?: entity.entityId + val friendlyName = entity.friendlyName val domainLabel = SUPPORTED_DOMAINS_WITH_STRING[entity.domain] ?.let { carContext.getString(it) } ?: entity.domain From d856a5c00dd34ddc0f3818ce7814ae58f2633e32 Mon Sep 17 00:00:00 2001 From: cddu33 <59371705+cddu33@users.noreply.github.com> Date: Thu, 7 May 2026 20:03:41 +0200 Subject: [PATCH 48/49] Update app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt Co-authored-by: Timothy <6560631+TimoPtr@users.noreply.github.com> --- .../companion/android/vehicle/ManageFavoritesVehicleScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index 4f4b30b04f8..5cdd16911e6 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -44,7 +44,7 @@ class ManageFavoritesVehicleScreen( private val toggleMutex = Mutex() init { - lifecycleScope.launch { + lifecycleScope.launch(Dispatcher.Default) { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { favoritesList = prefsRepository.getAutoFavorites() allEntities.collect { entityMap -> From e75da42efa00cb8261d00e3e03bffc2dd7744bd9 Mon Sep 17 00:00:00 2001 From: cddu33 Date: Thu, 7 May 2026 21:17:15 +0200 Subject: [PATCH 49/49] correction, change pagination and add search --- .../vehicle/ManageFavoritesVehicleScreen.kt | 430 +++++++++++++----- common/src/main/res/values/strings.xml | 2 + 2 files changed, 328 insertions(+), 104 deletions(-) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt index 586aaf4897a..92f885674d9 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/ManageFavoritesVehicleScreen.kt @@ -1,35 +1,54 @@ package io.homeassistant.companion.android.vehicle +import android.os.Build +import androidx.annotation.RequiresApi import androidx.car.app.CarContext import androidx.car.app.constraints.ConstraintManager +import androidx.car.app.model.Action +import androidx.car.app.model.CarColor +import androidx.car.app.model.CarIcon +import androidx.car.app.model.Header import androidx.car.app.model.ItemList import androidx.car.app.model.ListTemplate import androidx.car.app.model.Row +import androidx.car.app.model.SearchTemplate 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 com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import com.mikepenz.iconics.utils.sizeDp +import com.mikepenz.iconics.utils.toAndroidIconCompat 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.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext /** * 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. * + * Pagination prev/next controls live in the header so that the full list capacity is available + * for entity rows. A search action in the header opens a [SearchFavoritesVehicleScreen] for + * filtering by name. + * * This screen stays fully within the Car App API, making it compliant with Play Store * automotive distribution policies. */ +@RequiresApi(Build.VERSION_CODES.O) class ManageFavoritesVehicleScreen( carContext: CarContext, private val serverId: StateFlow, @@ -37,36 +56,42 @@ class ManageFavoritesVehicleScreen( private val prefsRepository: PrefsRepository, ) : BaseVehicleScreen(carContext) { - private var entities: List = emptyList() - private var favoritesList: List = emptyList() - private var isLoaded = false - private var page = 0 - private val toggleMutex = Mutex() + private data class UIState( + val entities: List = emptyList(), + val favoritesList: List = emptyList(), + val isLoaded: Boolean = false, + val page: Int = 0, + ) + + @Volatile + private var uiState = UIState() + private val stateMutex = Mutex() init { lifecycleScope.launch { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - favoritesList = prefsRepository.getAutoFavorites() + val initialFavorites = withContext(Dispatchers.IO) { prefsRepository.getAutoFavorites() } + stateMutex.withLock { + uiState = uiState.copy(favoritesList = initialFavorites) + } allEntities.collect { entityMap -> - val favoriteEntityIds = favoritesList - .asSequence() - .filter { it.serverId == serverId.value } - .map { it.entityId } - .toSet() - - val newEntities = entityMap.values - .filter { it.domain in SUPPORTED_DOMAINS_WITH_STRING } - .sortedWith( - compareByDescending { entity -> - favoriteEntityIds.contains(entity.entityId) - }.thenBy { it.attributes["friendly_name"]?.toString() ?: it.entityId }, - ) - val listChanged = newEntities.map { it.entityId } != entities.map { it.entityId } - if (listChanged) page = 0 - val shouldInvalidate = !isLoaded || listChanged - entities = newEntities - isLoaded = true - if (shouldInvalidate) invalidate() + withContext(Dispatchers.Default) { + stateMutex.withLock { + val state = uiState + val favoriteIds = favoriteEntityIdsForServer(state.favoritesList, serverId.value) + val newEntities = entityMap.values + .filter { it.domain in SUPPORTED_DOMAINS_WITH_STRING } + .sortedWith(compareByFavoriteThenName(favoriteIds)) + val listChanged = newEntities.map { it.entityId } != state.entities.map { it.entityId } + val shouldInvalidate = !state.isLoaded || listChanged + uiState = state.copy( + entities = newEntities, + isLoaded = true, + page = if (listChanged) 0 else state.page, + ) + if (shouldInvalidate) invalidate() + } + } } } } @@ -74,117 +99,247 @@ class ManageFavoritesVehicleScreen( override fun onDrivingOptimizedChanged(newState: Boolean) { if (newState) { - lifecycleScope.launch { - screenManager.pop() - } + screenManager.pop() } invalidate() } override fun onGetTemplate(): Template { + val state = uiState val listLimit = carContext.getCarService(ConstraintManager::class.java) .getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST) - val pageSlice = computePageSlice(entities.size, page, listLimit) - val pageEntities = if (isLoaded && pageSlice.fromIndex < entities.size) { - entities.subList(pageSlice.fromIndex, pageSlice.toIndex) + val pageSlice = computePageSlice(state.entities.size, state.page, listLimit) + val pageEntities = if (state.isLoaded && pageSlice.fromIndex < state.entities.size) { + state.entities.subList(pageSlice.fromIndex, pageSlice.toIndex) } else { emptyList() } return ListTemplate.Builder() - .setHeader(carContext.getHeaderBuilder(commonR.string.android_automotive_favorites).build()) - .setLoading(!isLoaded) + .setHeader(buildHeader(state, pageSlice)) + .setLoading(!state.isLoaded) .apply { - if (isLoaded) setSingleList(buildList(pageEntities, pageSlice).build()) + if (state.isLoaded) setSingleList(buildEntityList(state, pageEntities).build()) } .build() } - private fun buildList(pageEntities: List, pageSlice: PageSlice): ItemList.Builder { - val listBuilder = ItemList.Builder() - - if (pageSlice.hasPreviousPage) { - listBuilder.addItem( - buildNavigationRow(commonR.string.aa_previous_page) { - page-- - invalidate() - }, - ) + private fun buildHeader(state: UIState, pageSlice: PageSlice): Header { + val builder = carContext.getHeaderBuilder(commonR.string.android_automotive_favorites) + if (state.isLoaded) { + if (pageSlice.hasPreviousPage) { + builder.addEndHeaderAction( + Action.Builder() + .setIcon(carIcon(carContext, CommunityMaterial.Icon.cmd_chevron_left)) + .setTitle(carContext.getString(commonR.string.aa_previous_page)) + .setOnClickListener { + lifecycleScope.launch { + stateMutex.withLock { + uiState = uiState.copy(page = (uiState.page - 1).coerceAtLeast(0)) + invalidate() + } + } + } + .build(), + ) + } + if (pageSlice.hasNextPage) { + builder.addEndHeaderAction( + Action.Builder() + .setIcon(carIcon(carContext, CommunityMaterial.Icon.cmd_chevron_right)) + .setTitle(carContext.getString(commonR.string.aa_next_page)) + .setOnClickListener { + lifecycleScope.launch { + stateMutex.withLock { + uiState = uiState.copy(page = uiState.page + 1) + invalidate() + } + } + } + .build(), + ) + } } + builder.addEndHeaderAction( + Action.Builder() + .setIcon(carIcon(carContext, CommunityMaterial.Icon3.cmd_magnify)) + .setOnClickListener { + screenManager.push( + SearchFavoritesVehicleScreen(carContext, serverId, allEntities, prefsRepository), + ) + } + .build(), + ) + return builder.build() + } + private fun buildEntityList(state: UIState, pageEntities: List): ItemList.Builder { + val listBuilder = ItemList.Builder() pageEntities.forEach { entity -> - listBuilder.addItem(buildEntityRow(entity)) - } - - if (pageSlice.hasNextPage) { + val isFavorite = state.favoritesList.any { + it.serverId == serverId.value && it.entityId == entity.entityId + } listBuilder.addItem( - buildNavigationRow(commonR.string.aa_next_page) { - page++ - invalidate() + buildFavoriteEntityRow(carContext, entity, isFavorite) { isChecked -> + onFavoriteToggled(entity, isChecked) }, ) } - - if (isLoaded && entities.isEmpty()) { + if (state.entities.isEmpty()) { listBuilder.setNoItemsMessage(carContext.getString(commonR.string.no_supported_entities)) } - return listBuilder } - private fun buildNavigationRow(titleRes: Int, onClick: () -> Unit): Row = Row.Builder() - .setTitle(carContext.getString(titleRes)) - .setOnClickListener(onClick) - .build() + private fun onFavoriteToggled(entity: Entity, isChecked: Boolean) { + lifecycleScope.launch(Dispatchers.IO) { + stateMutex.withLock { + val newFavorites = persistFavoriteToggle( + prefsRepository, + serverId.value, + entity.entityId, + isChecked, + uiState.favoritesList, + ) + val favoriteIds = favoriteEntityIdsForServer(newFavorites, serverId.value) + uiState = uiState.copy( + favoritesList = newFavorites, + entities = uiState.entities.sortedWith(compareByFavoriteThenName(favoriteIds)), + ) + invalidate() + } + } + } +} + +/** + * A Car App screen showing a [SearchTemplate] over the same favorite-aware entity list as + * [ManageFavoritesVehicleScreen]. The user types to filter by friendly name or entity id and + * can toggle favorites on the matching rows. Pops itself when driving-optimized restrictions + * kick in (the search keyboard is unsafe while driving). + */ +@RequiresApi(Build.VERSION_CODES.O) +private class SearchFavoritesVehicleScreen( + carContext: CarContext, + private val serverId: StateFlow, + private val allEntities: Flow>, + private val prefsRepository: PrefsRepository, +) : BaseVehicleScreen(carContext) { + + private data class UIState( + val entities: List = emptyList(), + val favoritesList: List = emptyList(), + val query: String = "", + val isLoaded: Boolean = false, + ) - private fun buildEntityRow(entity: Entity): Row { - val isFavorite = favoritesList.any { - it.serverId == serverId.value && it.entityId == entity.entityId + @Volatile + private var uiState = UIState() + private val stateMutex = Mutex() + + private val searchCallback = object : SearchTemplate.SearchCallback { + override fun onSearchTextChanged(searchText: String) { + lifecycleScope.launch { + stateMutex.withLock { + if (uiState.query != searchText) { + uiState = uiState.copy(query = searchText) + invalidate() + } + } + } } - val friendlyName = entity.attributes["friendly_name"]?.toString() ?: entity.entityId - val domainLabel = SUPPORTED_DOMAINS_WITH_STRING[entity.domain] - ?.let { carContext.getString(it) } - ?: entity.domain - - return Row.Builder() - .setTitle(friendlyName) - .addText(domainLabel) - .setToggle( - Toggle.Builder { isChecked -> - lifecycleScope.launch { - toggleMutex.withLock { - val favorite = AutoFavorite( - serverId = serverId.value, - entityId = entity.entityId, - ) - if (isChecked) { - prefsRepository.addAutoFavorite(favorite) - } else { - prefsRepository.setAutoFavorites( - favoritesList.filterNot { it == favorite }, - ) - } - favoritesList = prefsRepository.getAutoFavorites() - val favoriteEntityIds = favoritesList - .filter { it.serverId == serverId.value } - .map { it.entityId } - .toSet() - entities = entities.sortedWith( - compareByDescending { it.entityId in favoriteEntityIds } - .thenBy { it.attributes["friendly_name"]?.toString() ?: it.entityId }, - ) - invalidate() + } + + init { + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + val initialFavorites = withContext(Dispatchers.IO) { prefsRepository.getAutoFavorites() } + stateMutex.withLock { + uiState = uiState.copy(favoritesList = initialFavorites) + } + allEntities.collect { entityMap -> + withContext(Dispatchers.Default) { + stateMutex.withLock { + val state = uiState + val favoriteIds = favoriteEntityIdsForServer(state.favoritesList, serverId.value) + val newEntities = entityMap.values + .filter { it.domain in SUPPORTED_DOMAINS_WITH_STRING } + .sortedWith(compareByFavoriteThenName(favoriteIds)) + val listChanged = newEntities.map { it.entityId } != state.entities.map { it.entityId } + val shouldInvalidate = !state.isLoaded || listChanged + uiState = state.copy(entities = newEntities, isLoaded = true) + if (shouldInvalidate) invalidate() } } } - .setChecked(isFavorite) - .build(), + } + } + } + + override fun onDrivingOptimizedChanged(newState: Boolean) { + if (newState) { + screenManager.pop() + } + invalidate() + } + + override fun onGetTemplate(): Template { + val state = uiState + val listLimit = carContext.getCarService(ConstraintManager::class.java) + .getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST) + val results = filterEntitiesByQuery(state.entities, state.query).take(listLimit) + + val itemListBuilder = ItemList.Builder() + results.forEach { entity -> + val isFavorite = state.favoritesList.any { + it.serverId == serverId.value && it.entityId == entity.entityId + } + itemListBuilder.addItem( + buildFavoriteEntityRow(carContext, entity, isFavorite) { isChecked -> + onFavoriteToggled(entity, isChecked) + }, ) + } + if (state.isLoaded && results.isEmpty()) { + val message = if (state.query.isBlank()) { + commonR.string.no_supported_entities + } else { + commonR.string.aa_search_no_results + } + itemListBuilder.setNoItemsMessage(carContext.getString(message)) + } + + return SearchTemplate.Builder(searchCallback) + .setHeaderAction(Action.BACK) + .setSearchHint(carContext.getString(commonR.string.aa_search_entities)) + .setShowKeyboardByDefault(true) + .setLoading(!state.isLoaded) + .apply { if (state.isLoaded) setItemList(itemListBuilder.build()) } .build() } + + private fun onFavoriteToggled(entity: Entity, isChecked: Boolean) { + lifecycleScope.launch(Dispatchers.IO) { + stateMutex.withLock { + val newFavorites = persistFavoriteToggle( + prefsRepository, + serverId.value, + entity.entityId, + isChecked, + uiState.favoritesList, + ) + val favoriteIds = favoriteEntityIdsForServer(newFavorites, serverId.value) + uiState = uiState.copy( + favoritesList = newFavorites, + entities = uiState.entities.sortedWith(compareByFavoriteThenName(favoriteIds)), + ) + invalidate() + } + } + } } -internal data class PageSlice( +private data class PageSlice( val fromIndex: Int, val toIndex: Int, val hasPreviousPage: Boolean, @@ -194,17 +349,84 @@ internal data class PageSlice( /** * Computes the slice of entities to display for the given page. * - * Always reserves 2 rows for navigation (previous/next), giving a consistent - * [itemsPerPage] across all pages and avoiding skipped entities. + * Pagination controls live in the header instead of consuming list rows, so [listLimit] is + * the full capacity available for entity rows. [page] is clamped against the current total so + * a stale page index can't produce an empty slice or an out-of-bounds [List.subList] read. */ -internal fun computePageSlice(totalItems: Int, page: Int, listLimit: Int): PageSlice { - val itemsPerPage = (listLimit - 2).coerceAtLeast(1) - val fromIndex = page * itemsPerPage - val toIndex = minOf(fromIndex + itemsPerPage, totalItems) +private fun computePageSlice(totalItems: Int, page: Int, listLimit: Int): PageSlice { + val itemsPerPage = listLimit.coerceAtLeast(1) + val maxPage = if (totalItems == 0) 0 else (totalItems - 1) / itemsPerPage + val safePage = page.coerceIn(0, maxPage) + val fromIndex = safePage * itemsPerPage + val toIndex = (fromIndex + itemsPerPage).coerceAtMost(totalItems) return PageSlice( fromIndex = fromIndex, toIndex = toIndex, - hasPreviousPage = page > 0, + hasPreviousPage = safePage > 0, hasNextPage = toIndex < totalItems, ) } + +private fun buildFavoriteEntityRow( + carContext: CarContext, + entity: Entity, + isFavorite: Boolean, + onCheckedChange: (Boolean) -> Unit, +): Row { + val friendlyName = entity.attributes["friendly_name"]?.toString() ?: entity.entityId + val domainLabel = SUPPORTED_DOMAINS_WITH_STRING[entity.domain] + ?.let { carContext.getString(it) } + ?: entity.domain + + return Row.Builder() + .setTitle(friendlyName) + .addText(domainLabel) + .setToggle( + Toggle.Builder { isChecked -> onCheckedChange(isChecked) } + .setChecked(isFavorite) + .build(), + ) + .build() +} + +private suspend fun persistFavoriteToggle( + prefsRepository: PrefsRepository, + serverId: Int, + entityId: String, + isChecked: Boolean, + currentFavorites: List, +): List { + val favorite = AutoFavorite(serverId = serverId, entityId = entityId) + if (isChecked) { + prefsRepository.addAutoFavorite(favorite) + } else { + prefsRepository.setAutoFavorites(currentFavorites.filterNot { it == favorite }) + } + return prefsRepository.getAutoFavorites() +} + +private fun favoriteEntityIdsForServer(favorites: List, serverId: Int): Set = + favorites.asSequence() + .filter { it.serverId == serverId } + .map { it.entityId } + .toSet() + +private fun compareByFavoriteThenName(favoriteEntityIds: Set): Comparator = + compareByDescending { it.entityId in favoriteEntityIds } + .thenBy { it.attributes["friendly_name"]?.toString() ?: it.entityId } + +private fun filterEntitiesByQuery(entities: List, query: String): List { + if (query.isBlank()) return entities + val needle = query.trim().lowercase() + return entities.filter { entity -> + val friendlyName = entity.attributes["friendly_name"]?.toString()?.lowercase() + friendlyName?.contains(needle) == true || entity.entityId.lowercase().contains(needle) + } +} + +@RequiresApi(Build.VERSION_CODES.O) +private fun carIcon(carContext: CarContext, icon: IIcon): CarIcon = CarIcon.Builder( + IconicsDrawable(carContext, icon).apply { sizeDp = 64 }.toAndroidIconCompat(), +) + .setTint(CarColor.DEFAULT) + .build() diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index a30fa1413c2..f159630be97 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1240,6 +1240,8 @@ Manage favorites Previous page Next page + Search entities + No matching entities Alarm Control Panels Triggered Disarmed