mirror of
https://github.com/home-assistant/android
synced 2024-07-22 10:54:12 +00:00
Android auto: Switch to GridTemplate for home screen and default to favorites if defined (#3694)
* Android auto: default to favorites if defined * Fix changing servers * Fix empty favorites list * Review comments * Account for extra grid items when showing favorites * Switch to grid view for home screen, show all domains when there are no favorites for the server * Review comments * Clean up and move some methods to util * Collect favorites with allEntities * Review comments * Move native mode logic back * Check distraction again in domain screen * Use base class to get distraction optimized variable * Send blank template if user is not logged in * Mark variables as private * Fix icon resolution for grid items
This commit is contained in:
parent
d9db8a06df
commit
4069d29d0a
211
app/src/main/java/io/homeassistant/companion/android/util/vehicle/GridItems.kt
Executable file
211
app/src/main/java/io/homeassistant/companion/android/util/vehicle/GridItems.kt
Executable file
|
@ -0,0 +1,211 @@
|
|||
package io.homeassistant.companion.android.util.vehicle
|
||||
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.ScreenManager
|
||||
import androidx.car.app.model.CarColor
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.GridItem
|
||||
import androidx.car.app.model.ItemList
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
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
|
||||
import io.homeassistant.companion.android.common.data.integration.Entity
|
||||
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
||||
import io.homeassistant.companion.android.common.data.integration.domain
|
||||
import io.homeassistant.companion.android.common.data.integration.getIcon
|
||||
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
|
||||
import io.homeassistant.companion.android.common.data.servers.ServerManager
|
||||
import io.homeassistant.companion.android.common.util.capitalize
|
||||
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.MainVehicleScreen
|
||||
import io.homeassistant.companion.android.vehicle.MapVehicleScreen
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
private const val TAG = "GridItems"
|
||||
|
||||
fun getChangeServerGridItem(
|
||||
carContext: CarContext,
|
||||
screenManager: ScreenManager,
|
||||
serverManager: ServerManager,
|
||||
serverId: StateFlow<Int>,
|
||||
onChangeServer: (Int) -> Unit
|
||||
): GridItem.Builder {
|
||||
return GridItem.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.aa_change_server))
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconicsDrawable(
|
||||
carContext,
|
||||
CommunityMaterial.Icon2.cmd_home_switch
|
||||
).apply {
|
||||
sizeDp = 64
|
||||
}.toAndroidIconCompat()
|
||||
)
|
||||
.setTint(CarColor.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
setOnClickListener {
|
||||
Log.i(TAG, "Change server clicked")
|
||||
screenManager.pushForResult(
|
||||
ChangeServerScreen(
|
||||
carContext,
|
||||
serverManager,
|
||||
serverId
|
||||
)
|
||||
) {
|
||||
it?.toString()?.toIntOrNull()?.let { serverId ->
|
||||
onChangeServer(serverId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun getNavigationGridItem(
|
||||
carContext: CarContext,
|
||||
screenManager: ScreenManager,
|
||||
integrationRepository: IntegrationRepository,
|
||||
allEntities: Flow<Map<String, Entity<*>>>
|
||||
): GridItem.Builder {
|
||||
return GridItem.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.aa_navigation))
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconicsDrawable(
|
||||
carContext,
|
||||
CommunityMaterial.Icon3.cmd_map_outline
|
||||
).apply {
|
||||
sizeDp = 64
|
||||
}.toAndroidIconCompat()
|
||||
)
|
||||
.setTint(CarColor.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
setOnClickListener {
|
||||
Log.i(TAG, "Navigation clicked")
|
||||
screenManager.push(
|
||||
MapVehicleScreen(
|
||||
carContext,
|
||||
integrationRepository,
|
||||
allEntities.map { it.values.filter { entity -> entity.domain in MainVehicleScreen.MAP_DOMAINS } }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun getDomainList(
|
||||
domains: MutableSet<String>,
|
||||
carContext: CarContext,
|
||||
screenManager: ScreenManager,
|
||||
serverManager: ServerManager,
|
||||
serverId: StateFlow<Int>,
|
||||
prefsRepository: PrefsRepository,
|
||||
allEntities: Flow<Map<String, Entity<*>>>
|
||||
): ItemList.Builder {
|
||||
val listBuilder = ItemList.Builder()
|
||||
domains.forEach { domain ->
|
||||
val friendlyDomain =
|
||||
MainVehicleScreen.SUPPORTED_DOMAINS_WITH_STRING[domain]?.let { carContext.getString(it) }
|
||||
?: domain.split("_").joinToString(" ") { word ->
|
||||
word.capitalize(Locale.getDefault())
|
||||
}
|
||||
val icon = Entity(
|
||||
"$domain.ha_android_placeholder",
|
||||
"",
|
||||
mapOf<Any, Any>(),
|
||||
Calendar.getInstance(),
|
||||
Calendar.getInstance(),
|
||||
null
|
||||
).getIcon(carContext)
|
||||
|
||||
listBuilder.addItem(
|
||||
GridItem.Builder().apply {
|
||||
if (icon != null) {
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconicsDrawable(carContext, icon)
|
||||
.apply {
|
||||
sizeDp = 64
|
||||
}.toAndroidIconCompat()
|
||||
)
|
||||
.setTint(CarColor.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
.setTitle(friendlyDomain)
|
||||
.setOnClickListener {
|
||||
Log.i(TAG, "Domain:$domain clicked")
|
||||
screenManager.push(
|
||||
EntityGridVehicleScreen(
|
||||
carContext,
|
||||
serverManager,
|
||||
serverId,
|
||||
prefsRepository,
|
||||
serverManager.integrationRepository(serverId.value),
|
||||
friendlyDomain,
|
||||
domains,
|
||||
allEntities.map { it.values.filter { entity -> entity.domain == domain } },
|
||||
allEntities
|
||||
) { }
|
||||
)
|
||||
}
|
||||
.build()
|
||||
)
|
||||
}
|
||||
return listBuilder
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun getDomainsGridItem(
|
||||
carContext: CarContext,
|
||||
screenManager: ScreenManager,
|
||||
serverManager: ServerManager,
|
||||
integrationRepository: IntegrationRepository,
|
||||
serverId: StateFlow<Int>,
|
||||
allEntities: Flow<Map<String, Entity<*>>>,
|
||||
prefsRepository: PrefsRepository
|
||||
): GridItem.Builder {
|
||||
return GridItem.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.all_entities))
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconicsDrawable(
|
||||
carContext,
|
||||
CommunityMaterial.Icon3.cmd_view_list
|
||||
).apply {
|
||||
sizeDp = 64
|
||||
}.toAndroidIconCompat()
|
||||
)
|
||||
.setTint(CarColor.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
setOnClickListener {
|
||||
Log.i(TAG, "Categories clicked")
|
||||
screenManager.push(
|
||||
DomainListScreen(
|
||||
carContext,
|
||||
serverManager,
|
||||
integrationRepository,
|
||||
serverId,
|
||||
allEntities,
|
||||
prefsRepository
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package io.homeassistant.companion.android.util.vehicle
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.ActionStrip
|
||||
import io.homeassistant.companion.android.common.R
|
||||
import io.homeassistant.companion.android.launch.LaunchActivity
|
||||
|
||||
private const val TAG = "NativeActionStrip"
|
||||
|
||||
fun nativeModeActionStrip(carContext: CarContext): ActionStrip {
|
||||
return ActionStrip.Builder().addAction(
|
||||
Action.Builder()
|
||||
.setTitle(carContext.getString(R.string.aa_launch_native))
|
||||
.setOnClickListener {
|
||||
startNativeActivity(carContext)
|
||||
}.build()
|
||||
).build()
|
||||
}
|
||||
|
||||
fun startNativeActivity(carContext: CarContext) {
|
||||
Log.i(TAG, "Starting login activity")
|
||||
with(carContext) {
|
||||
startActivity(
|
||||
Intent(
|
||||
carContext,
|
||||
LaunchActivity::class.java
|
||||
).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
)
|
||||
if (carContext.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
|
||||
finishCarApp()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package io.homeassistant.companion.android.vehicle
|
||||
|
||||
import android.car.Car
|
||||
import android.car.drivingstate.CarUxRestrictionsManager
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
||||
abstract class BaseVehicleScreen(
|
||||
carContext: CarContext
|
||||
) : Screen(carContext) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "BaseVehicle"
|
||||
}
|
||||
private var car: Car? = null
|
||||
private var carRestrictionManager: CarUxRestrictionsManager? = null
|
||||
protected val isDrivingOptimized
|
||||
get() = car?.let {
|
||||
(
|
||||
it.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE) as CarUxRestrictionsManager
|
||||
).getCurrentCarUxRestrictions().isRequiresDistractionOptimization()
|
||||
} ?: false
|
||||
|
||||
init {
|
||||
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
registerAutomotiveRestrictionListener()
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
carRestrictionManager?.unregisterListener()
|
||||
car?.disconnect()
|
||||
car = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
abstract fun onDrivingOptimizedChanged(newState: Boolean)
|
||||
|
||||
private fun registerAutomotiveRestrictionListener() {
|
||||
if (carContext.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
|
||||
Log.i(TAG, "Register for Automotive Restrictions")
|
||||
car = Car.createCar(carContext)
|
||||
carRestrictionManager =
|
||||
car?.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE) as CarUxRestrictionsManager
|
||||
val listener =
|
||||
CarUxRestrictionsManager.OnUxRestrictionsChangedListener { restrictions ->
|
||||
onDrivingOptimizedChanged(restrictions.isRequiresDistractionOptimization())
|
||||
}
|
||||
carRestrictionManager?.registerListener(listener)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package io.homeassistant.companion.android.vehicle
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.GridTemplate
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.homeassistant.companion.android.BuildConfig
|
||||
import io.homeassistant.companion.android.common.R
|
||||
import io.homeassistant.companion.android.common.data.integration.Entity
|
||||
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
||||
import io.homeassistant.companion.android.common.data.integration.domain
|
||||
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
|
||||
import io.homeassistant.companion.android.common.data.servers.ServerManager
|
||||
import io.homeassistant.companion.android.util.vehicle.getDomainList
|
||||
import io.homeassistant.companion.android.util.vehicle.nativeModeActionStrip
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
class DomainListScreen(
|
||||
carContext: CarContext,
|
||||
val serverManager: ServerManager,
|
||||
val integrationRepository: IntegrationRepository,
|
||||
private val serverId: StateFlow<Int>,
|
||||
private val allEntities: Flow<Map<String, Entity<*>>>,
|
||||
private val prefsRepository: PrefsRepository
|
||||
) : BaseVehicleScreen(carContext) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "DomainList"
|
||||
}
|
||||
|
||||
private val domains = mutableSetOf<String>()
|
||||
|
||||
override fun onDrivingOptimizedChanged(newState: Boolean) {
|
||||
invalidate()
|
||||
}
|
||||
|
||||
init {
|
||||
lifecycleScope.launch {
|
||||
allEntities.collect { entities ->
|
||||
val newDomains = entities.values
|
||||
.map { it.domain }
|
||||
.distinct()
|
||||
.filter { it in MainVehicleScreen.SUPPORTED_DOMAINS }
|
||||
.toSet()
|
||||
if (newDomains.size != domains.size || newDomains != domains) {
|
||||
domains.clear()
|
||||
domains.addAll(newDomains)
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
val isAutomotive = carContext.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
|
||||
val domainList = getDomainList(
|
||||
domains,
|
||||
carContext,
|
||||
screenManager,
|
||||
serverManager,
|
||||
serverId,
|
||||
prefsRepository,
|
||||
allEntities
|
||||
)
|
||||
|
||||
return GridTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.all_entities))
|
||||
setHeaderAction(Action.BACK)
|
||||
if (isAutomotive && !isDrivingOptimized && BuildConfig.FLAVOR != "full") {
|
||||
setActionStrip(nativeModeActionStrip(carContext))
|
||||
}
|
||||
val domainBuild = domainList.build()
|
||||
if (domainBuild.items.isEmpty()) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
setLoading(false)
|
||||
setSingleList(domainBuild)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ import com.mikepenz.iconics.IconicsDrawable
|
|||
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
|
||||
import io.homeassistant.companion.android.common.data.integration.Entity
|
||||
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
||||
import io.homeassistant.companion.android.common.data.integration.friendlyName
|
||||
|
@ -27,23 +28,38 @@ import io.homeassistant.companion.android.common.data.integration.friendlyState
|
|||
import io.homeassistant.companion.android.common.data.integration.getIcon
|
||||
import io.homeassistant.companion.android.common.data.integration.isExecuting
|
||||
import io.homeassistant.companion.android.common.data.integration.onPressed
|
||||
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
|
||||
import io.homeassistant.companion.android.common.data.servers.ServerManager
|
||||
import io.homeassistant.companion.android.util.vehicle.getChangeServerGridItem
|
||||
import io.homeassistant.companion.android.util.vehicle.getDomainList
|
||||
import io.homeassistant.companion.android.util.vehicle.getDomainsGridItem
|
||||
import io.homeassistant.companion.android.util.vehicle.getNavigationGridItem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
class EntityGridVehicleScreen(
|
||||
carContext: CarContext,
|
||||
val serverManager: ServerManager,
|
||||
val serverId: StateFlow<Int>,
|
||||
val prefsRepository: PrefsRepository,
|
||||
val integrationRepository: IntegrationRepository,
|
||||
val title: String,
|
||||
val entitiesFlow: Flow<List<Entity<*>>>
|
||||
private val domains: MutableSet<String>,
|
||||
private val entitiesFlow: Flow<List<Entity<*>>>,
|
||||
private val allEntities: Flow<Map<String, Entity<*>>>,
|
||||
private val onChangeServer: (Int) -> Unit
|
||||
) : Screen(carContext) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "EntityGridVehicleScreen"
|
||||
}
|
||||
|
||||
var loading = true
|
||||
private var loading = true
|
||||
var entities: List<Entity<*>> = listOf()
|
||||
private val isFavorites = title == carContext.getString(R.string.favorites)
|
||||
private val shouldSwitchServers = serverManager.defaultServers.size > 1
|
||||
|
||||
init {
|
||||
lifecycleScope.launch {
|
||||
|
@ -58,13 +74,76 @@ class EntityGridVehicleScreen(
|
|||
}
|
||||
}
|
||||
|
||||
fun getEntityGridItems(entities: List<Entity<*>>): ItemList.Builder {
|
||||
val listBuilder = if (entities.isNotEmpty()) {
|
||||
createEntityGrid(entities)
|
||||
} else {
|
||||
getDomainList(
|
||||
domains,
|
||||
carContext,
|
||||
screenManager,
|
||||
serverManager,
|
||||
serverId,
|
||||
prefsRepository,
|
||||
allEntities
|
||||
)
|
||||
}
|
||||
if (isFavorites) {
|
||||
listBuilder.addItem(
|
||||
getNavigationGridItem(
|
||||
carContext,
|
||||
screenManager,
|
||||
integrationRepository,
|
||||
allEntities
|
||||
).build()
|
||||
)
|
||||
listBuilder.addItem(
|
||||
getDomainsGridItem(
|
||||
carContext,
|
||||
screenManager,
|
||||
serverManager,
|
||||
integrationRepository,
|
||||
serverId,
|
||||
allEntities,
|
||||
prefsRepository
|
||||
).build()
|
||||
)
|
||||
if (shouldSwitchServers) {
|
||||
listBuilder.addItem(
|
||||
getChangeServerGridItem(
|
||||
carContext,
|
||||
screenManager,
|
||||
serverManager,
|
||||
serverId
|
||||
) { onChangeServer(it) }.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
return listBuilder
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
val entityGrid = getEntityGridItems(entities)
|
||||
|
||||
return GridTemplate.Builder().apply {
|
||||
setTitle(title)
|
||||
setHeaderAction(Action.BACK)
|
||||
if (loading) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
setLoading(false)
|
||||
setSingleList(entityGrid.build())
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun createEntityGrid(entities: List<Entity<*>>): ItemList.Builder {
|
||||
val listBuilder = ItemList.Builder()
|
||||
val manager = carContext.getCarService(ConstraintManager::class.java)
|
||||
val gridLimit = manager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_GRID)
|
||||
|
||||
val listBuilder = ItemList.Builder()
|
||||
val extraGrid = if (shouldSwitchServers) 3 else 2
|
||||
entities.forEachIndexed { index, entity ->
|
||||
if (index >= gridLimit) {
|
||||
if (index >= (gridLimit - if (isFavorites) extraGrid else 0)) {
|
||||
Log.i(TAG, "Grid limit ($gridLimit) reached, not adding more entities (${entities.size}) for $title ")
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
@ -97,16 +176,6 @@ class EntityGridVehicleScreen(
|
|||
}
|
||||
listBuilder.addItem(gridItem.build())
|
||||
}
|
||||
|
||||
return GridTemplate.Builder().apply {
|
||||
setTitle(title)
|
||||
setHeaderAction(Action.BACK)
|
||||
if (loading) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
setLoading(false)
|
||||
setSingleList(listBuilder.build())
|
||||
}
|
||||
}.build()
|
||||
return listBuilder
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +1,32 @@
|
|||
package io.homeassistant.companion.android.vehicle
|
||||
|
||||
import android.car.Car
|
||||
import android.car.drivingstate.CarUxRestrictionsManager
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.ActionStrip
|
||||
import androidx.car.app.model.CarColor
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.GridTemplate
|
||||
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.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
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.BuildConfig
|
||||
import io.homeassistant.companion.android.common.data.authentication.SessionState
|
||||
import io.homeassistant.companion.android.common.data.integration.Entity
|
||||
import io.homeassistant.companion.android.common.data.integration.domain
|
||||
import io.homeassistant.companion.android.common.data.integration.getIcon
|
||||
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
|
||||
import io.homeassistant.companion.android.common.data.servers.ServerManager
|
||||
import io.homeassistant.companion.android.common.util.capitalize
|
||||
import io.homeassistant.companion.android.launch.LaunchActivity
|
||||
import io.homeassistant.companion.android.util.vehicle.getChangeServerGridItem
|
||||
import io.homeassistant.companion.android.util.vehicle.getDomainList
|
||||
import io.homeassistant.companion.android.util.vehicle.getNavigationGridItem
|
||||
import io.homeassistant.companion.android.util.vehicle.nativeModeActionStrip
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
|
@ -52,12 +37,12 @@ class MainVehicleScreen(
|
|||
private val allEntities: Flow<Map<String, Entity<*>>>,
|
||||
private val prefsRepository: PrefsRepository,
|
||||
private val onChangeServer: (Int) -> Unit
|
||||
) : Screen(carContext) {
|
||||
) : BaseVehicleScreen(carContext) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MainVehicleScreen"
|
||||
|
||||
private val SUPPORTED_DOMAINS_WITH_STRING = mapOf(
|
||||
val SUPPORTED_DOMAINS_WITH_STRING = mapOf(
|
||||
"button" to commonR.string.buttons,
|
||||
"cover" to commonR.string.covers,
|
||||
"input_boolean" to commonR.string.input_booleans,
|
||||
|
@ -70,7 +55,7 @@ class MainVehicleScreen(
|
|||
)
|
||||
val SUPPORTED_DOMAINS = SUPPORTED_DOMAINS_WITH_STRING.keys
|
||||
|
||||
private val MAP_DOMAINS = listOf(
|
||||
val MAP_DOMAINS = listOf(
|
||||
"device_tracker",
|
||||
"person",
|
||||
"sensor",
|
||||
|
@ -78,17 +63,11 @@ class MainVehicleScreen(
|
|||
)
|
||||
}
|
||||
|
||||
private var favoriteEntities = flowOf<List<Entity<*>>>()
|
||||
private var entityList: List<Entity<*>> = listOf()
|
||||
private var favoritesList = emptyList<String>()
|
||||
private var isLoggedIn: Boolean? = null
|
||||
private val domains = mutableSetOf<String>()
|
||||
private var car: Car? = null
|
||||
private var carRestrictionManager: CarUxRestrictionsManager? = null
|
||||
private val iDrivingOptimized
|
||||
get() = car?.let {
|
||||
(
|
||||
it.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE) as CarUxRestrictionsManager
|
||||
).getCurrentCarUxRestrictions().isRequiresDistractionOptimization()
|
||||
} ?: false
|
||||
|
||||
private val isAutomotive get() = carContext.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
|
||||
|
||||
|
@ -118,174 +97,78 @@ class MainVehicleScreen(
|
|||
domains.addAll(newDomains)
|
||||
invalidate()
|
||||
}
|
||||
entityList = getFavoritesList(entities)
|
||||
}
|
||||
favoriteEntities = allEntities.map { getFavoritesList(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
registerAutomotiveRestrictionListener()
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
carRestrictionManager?.unregisterListener()
|
||||
car?.disconnect()
|
||||
car = null
|
||||
}
|
||||
})
|
||||
override fun onDrivingOptimizedChanged(newState: Boolean) {
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
val listBuilder = ItemList.Builder()
|
||||
if (favoritesList.isNotEmpty()) {
|
||||
listBuilder.addItem(
|
||||
Row.Builder().apply {
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconicsDrawable(carContext, CommunityMaterial.Icon3.cmd_star).apply {
|
||||
sizeDp = 48
|
||||
}.toAndroidIconCompat()
|
||||
)
|
||||
.setTint(CarColor.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
setTitle(carContext.getString(commonR.string.favorites))
|
||||
setOnClickListener {
|
||||
Log.i(TAG, "Favorites clicked: $favoritesList, current server: ${serverId.value}")
|
||||
screenManager.push(
|
||||
EntityGridVehicleScreen(
|
||||
carContext,
|
||||
serverManager.integrationRepository(serverId.value),
|
||||
carContext.getString(commonR.string.favorites),
|
||||
allEntities.map { it.values.filter { entity -> favoritesList.contains("${serverId.value}-${entity.entityId}") }.sortedBy { entity -> favoritesList.indexOf("${serverId.value}-${entity.entityId}") } }
|
||||
)
|
||||
)
|
||||
}
|
||||
}.build()
|
||||
)
|
||||
if (isLoggedIn != true) {
|
||||
return GridTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(commonR.string.app_name))
|
||||
setHeaderAction(Action.APP_ICON)
|
||||
setLoading(true)
|
||||
}.build()
|
||||
}
|
||||
domains.forEach { domain ->
|
||||
val friendlyDomain =
|
||||
SUPPORTED_DOMAINS_WITH_STRING[domain]?.let { carContext.getString(it) }
|
||||
?: domain.split("_").joinToString(" ") { word ->
|
||||
word.capitalize(Locale.getDefault())
|
||||
}
|
||||
val icon = Entity(
|
||||
"$domain.ha_android_placeholder",
|
||||
"",
|
||||
mapOf<Any, Any>(),
|
||||
Calendar.getInstance(),
|
||||
Calendar.getInstance(),
|
||||
null
|
||||
).getIcon(carContext)
|
||||
|
||||
listBuilder.addItem(
|
||||
Row.Builder().apply {
|
||||
if (icon != null) {
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconicsDrawable(carContext, icon)
|
||||
.apply {
|
||||
sizeDp = 48
|
||||
}.toAndroidIconCompat()
|
||||
)
|
||||
.setTint(CarColor.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
.setTitle(friendlyDomain)
|
||||
.setOnClickListener {
|
||||
Log.i(TAG, "Domain:$domain clicked")
|
||||
screenManager.push(
|
||||
EntityGridVehicleScreen(
|
||||
carContext,
|
||||
serverManager.integrationRepository(serverId.value),
|
||||
friendlyDomain,
|
||||
allEntities.map { it.values.filter { entity -> entity.domain == domain } }
|
||||
)
|
||||
)
|
||||
}
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
listBuilder.addItem(
|
||||
Row.Builder()
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconicsDrawable(
|
||||
carContext,
|
||||
CommunityMaterial.Icon3.cmd_map_outline
|
||||
).apply {
|
||||
sizeDp = 48
|
||||
}.toAndroidIconCompat()
|
||||
)
|
||||
.setTint(CarColor.DEFAULT)
|
||||
.build()
|
||||
val listBuilder = if (favoritesList.isNotEmpty()) {
|
||||
EntityGridVehicleScreen(
|
||||
carContext,
|
||||
serverManager,
|
||||
serverId,
|
||||
prefsRepository,
|
||||
serverManager.integrationRepository(serverId.value),
|
||||
carContext.getString(commonR.string.favorites),
|
||||
domains,
|
||||
favoriteEntities,
|
||||
allEntities
|
||||
) { onChangeServer(it) }.getEntityGridItems(entityList)
|
||||
} else {
|
||||
var builder = ItemList.Builder()
|
||||
if (domains.isNotEmpty()) {
|
||||
builder = getDomainList(
|
||||
domains,
|
||||
carContext,
|
||||
screenManager,
|
||||
serverManager,
|
||||
serverId,
|
||||
prefsRepository,
|
||||
allEntities
|
||||
)
|
||||
.setTitle(carContext.getString(commonR.string.aa_navigation))
|
||||
.setOnClickListener {
|
||||
Log.i(TAG, "Navigation clicked")
|
||||
screenManager.push(
|
||||
MapVehicleScreen(
|
||||
carContext,
|
||||
serverManager.integrationRepository(serverId.value),
|
||||
allEntities.map { it.values.filter { entity -> entity.domain in MAP_DOMAINS } }
|
||||
)
|
||||
)
|
||||
}
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
if (serverManager.defaultServers.size > 1) {
|
||||
listBuilder.addItem(
|
||||
Row.Builder()
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconicsDrawable(
|
||||
carContext,
|
||||
CommunityMaterial.Icon2.cmd_home_switch
|
||||
).apply {
|
||||
sizeDp = 48
|
||||
}.toAndroidIconCompat()
|
||||
)
|
||||
.setTint(CarColor.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
.setTitle(carContext.getString(commonR.string.aa_change_server))
|
||||
.setOnClickListener {
|
||||
Log.i(TAG, "Change server clicked")
|
||||
screenManager.pushForResult(
|
||||
ChangeServerScreen(
|
||||
carContext,
|
||||
serverManager,
|
||||
serverId
|
||||
)
|
||||
) {
|
||||
it?.toString()?.toIntOrNull()?.let { serverId ->
|
||||
onChangeServer(serverId)
|
||||
}
|
||||
}
|
||||
}
|
||||
.build()
|
||||
builder.addItem(
|
||||
getNavigationGridItem(
|
||||
carContext,
|
||||
screenManager,
|
||||
serverManager.integrationRepository(serverId.value),
|
||||
allEntities
|
||||
).build()
|
||||
)
|
||||
|
||||
if (serverManager.defaultServers.size > 1) {
|
||||
builder.addItem(
|
||||
getChangeServerGridItem(
|
||||
carContext,
|
||||
screenManager,
|
||||
serverManager,
|
||||
serverId
|
||||
) { onChangeServer(it) }.build()
|
||||
)
|
||||
}
|
||||
builder
|
||||
}
|
||||
|
||||
return ListTemplate.Builder().apply {
|
||||
return GridTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(commonR.string.app_name))
|
||||
setHeaderAction(Action.APP_ICON)
|
||||
if (isAutomotive && !iDrivingOptimized && BuildConfig.FLAVOR != "full") {
|
||||
setActionStrip(
|
||||
ActionStrip.Builder().addAction(
|
||||
Action.Builder()
|
||||
.setTitle(carContext.getString(commonR.string.aa_launch_native))
|
||||
.setOnClickListener {
|
||||
startNativeActivity()
|
||||
}.build()
|
||||
).build()
|
||||
)
|
||||
if (isAutomotive && !isDrivingOptimized && BuildConfig.FLAVOR != "full") {
|
||||
setActionStrip(nativeModeActionStrip(carContext))
|
||||
}
|
||||
if (domains.isEmpty()) {
|
||||
setLoading(true)
|
||||
|
@ -296,34 +179,8 @@ class MainVehicleScreen(
|
|||
}.build()
|
||||
}
|
||||
|
||||
private fun registerAutomotiveRestrictionListener() {
|
||||
if (carContext.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
|
||||
Log.i(TAG, "Register for Automotive Restrictions")
|
||||
car = Car.createCar(carContext)
|
||||
carRestrictionManager =
|
||||
car?.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE) as CarUxRestrictionsManager
|
||||
val listener =
|
||||
CarUxRestrictionsManager.OnUxRestrictionsChangedListener { restrictions ->
|
||||
invalidate()
|
||||
}
|
||||
carRestrictionManager?.registerListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startNativeActivity() {
|
||||
Log.i(TAG, "Starting login activity")
|
||||
with(carContext) {
|
||||
startActivity(
|
||||
Intent(
|
||||
carContext,
|
||||
LaunchActivity::class.java
|
||||
).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
)
|
||||
if (isAutomotive) {
|
||||
finishCarApp()
|
||||
}
|
||||
}
|
||||
private fun getFavoritesList(entities: Map<String, Entity<*>>): List<Entity<*>> {
|
||||
return entities.values.filter { entity -> favoritesList.contains("${serverId.value}-${entity.entityId}") }
|
||||
.sortedBy { entity -> favoritesList.indexOf("${serverId.value}-${entity.entityId}") }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -200,7 +200,7 @@ class PrefsRepositoryImpl @Inject constructor(
|
|||
}
|
||||
|
||||
override suspend fun getAutoFavorites(): List<String> {
|
||||
return localStorage.getString(PREF_AUTO_FAVORITES)?.removeSurrounding("[", "]")?.split(", ") ?: emptyList()
|
||||
return localStorage.getString(PREF_AUTO_FAVORITES)?.removeSurrounding("[", "]")?.split(", ")?.filter { it.isNotBlank() } ?: emptyList()
|
||||
}
|
||||
|
||||
override suspend fun setAutoFavorites(favorites: List<String>) {
|
||||
|
|
Loading…
Reference in a new issue