mirror of
https://github.com/home-assistant/android
synced 2024-10-15 12:32:54 +00:00
Bunch of Android Auto enhancements (#3225)
* Bunch of Android Auto enhancements * Bump icon sizes. * Formatting. * Review Fixes. * Ooops. * Missed size.
This commit is contained in:
parent
3dfdc72588
commit
1e910e7ec7
2
.github/workflows/beta.yml
vendored
2
.github/workflows/beta.yml
vendored
|
@ -94,6 +94,8 @@ jobs:
|
|||
play_publish:
|
||||
name: Play Publish
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: playstore_deploy
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
|
|
@ -12,7 +12,9 @@ import androidx.car.app.model.GridItem
|
|||
import androidx.car.app.model.GridTemplate
|
||||
import androidx.car.app.model.ItemList
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.lifecycle.Lifecycle
|
||||
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
|
||||
|
@ -23,6 +25,7 @@ import io.homeassistant.companion.android.common.data.integration.friendlyName
|
|||
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.onPressed
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
|
@ -30,18 +33,22 @@ class EntityGridVehicleScreen(
|
|||
carContext: CarContext,
|
||||
val integrationRepository: IntegrationRepository,
|
||||
val title: String,
|
||||
val entities: MutableMap<String, Entity<*>>,
|
||||
val entitiesFlow: Flow<List<Entity<*>>>,
|
||||
) : Screen(carContext) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "EntityGridVehicleScreen"
|
||||
}
|
||||
|
||||
var loading = true
|
||||
var entities: List<Entity<*>> = listOf()
|
||||
|
||||
init {
|
||||
lifecycleScope.launch {
|
||||
integrationRepository.getEntityUpdates()?.collect { entity ->
|
||||
if (entities.containsKey(entity.entityId)) {
|
||||
entities[entity.entityId] = entity
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
entitiesFlow.collect {
|
||||
loading = false
|
||||
entities = it
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +57,7 @@ class EntityGridVehicleScreen(
|
|||
|
||||
override fun onGetTemplate(): Template {
|
||||
val listBuilder = ItemList.Builder()
|
||||
entities.forEach { (entityId, entity) ->
|
||||
entities.forEach { entity ->
|
||||
val icon = entity.getIcon(carContext) ?: CommunityMaterial.Icon.cmd_cloud_question
|
||||
listBuilder.addItem(
|
||||
GridItem.Builder()
|
||||
|
@ -67,7 +74,7 @@ class EntityGridVehicleScreen(
|
|||
.build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
Log.i(TAG, "$entityId clicked")
|
||||
Log.i(TAG, "${entity.entityId} clicked")
|
||||
lifecycleScope.launch {
|
||||
entity.onPressed(integrationRepository)
|
||||
}
|
||||
|
@ -76,10 +83,15 @@ class EntityGridVehicleScreen(
|
|||
)
|
||||
}
|
||||
|
||||
return GridTemplate.Builder()
|
||||
.setTitle(title)
|
||||
.setHeaderAction(Action.BACK)
|
||||
.setSingleList(listBuilder.build())
|
||||
.build()
|
||||
return GridTemplate.Builder().apply {
|
||||
setTitle(title)
|
||||
setHeaderAction(Action.BACK)
|
||||
if (loading) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
setLoading(false)
|
||||
setSingleList(listBuilder.build())
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,24 +3,40 @@ package io.homeassistant.companion.android.vehicle
|
|||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.car.app.CarAppService
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.Session
|
||||
import androidx.car.app.SessionInfo
|
||||
import androidx.car.app.validation.HostValidator
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository
|
||||
import io.homeassistant.companion.android.common.data.integration.Entity
|
||||
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import okhttp3.internal.toImmutableMap
|
||||
import javax.inject.Inject
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@AndroidEntryPoint
|
||||
class HaCarAppService : CarAppService() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "HaCarAppService"
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var integrationRepository: IntegrationRepository
|
||||
|
||||
@Inject
|
||||
lateinit var authenticationRepository: AuthenticationRepository
|
||||
|
||||
override fun createHostValidator(): HostValidator {
|
||||
return if (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0) {
|
||||
HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
|
||||
|
@ -33,8 +49,35 @@ class HaCarAppService : CarAppService() {
|
|||
|
||||
override fun onCreateSession(sessionInfo: SessionInfo): Session {
|
||||
return object : Session() {
|
||||
|
||||
private val allEntities: Flow<Map<String, Entity<*>>> = flow {
|
||||
emit(emptyMap())
|
||||
val entities: MutableMap<String, Entity<*>>? =
|
||||
integrationRepository.getEntities()
|
||||
?.associate { it.entityId to it }
|
||||
?.toMutableMap()
|
||||
if (entities != null) {
|
||||
emit(entities.toImmutableMap())
|
||||
integrationRepository.getEntityUpdates()?.collect { entity ->
|
||||
entities[entity.entityId] = entity
|
||||
emit(entities.toImmutableMap())
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "No entities found?")
|
||||
}
|
||||
}.shareIn(
|
||||
lifecycleScope,
|
||||
SharingStarted.WhileSubscribed(10_000),
|
||||
1
|
||||
)
|
||||
|
||||
override fun onCreateScreen(intent: Intent): Screen {
|
||||
return MainVehicleScreen(carContext, integrationRepository)
|
||||
return MainVehicleScreen(
|
||||
carContext,
|
||||
integrationRepository,
|
||||
authenticationRepository,
|
||||
allEntities
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,48 @@
|
|||
package io.homeassistant.companion.android.vehicle
|
||||
|
||||
import android.content.Intent
|
||||
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.CarColor
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.ItemList
|
||||
import androidx.car.app.model.ListTemplate
|
||||
import androidx.car.app.model.MessageTemplate
|
||||
import androidx.car.app.model.ParkedOnlyOnClickListener
|
||||
import androidx.car.app.model.Row
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.lifecycle.Lifecycle
|
||||
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.common.data.authentication.AuthenticationRepository
|
||||
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.IntegrationRepository
|
||||
import io.homeassistant.companion.android.common.data.integration.domain
|
||||
import io.homeassistant.companion.android.common.data.integration.getIcon
|
||||
import io.homeassistant.companion.android.launch.LaunchActivity
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
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)
|
||||
class MainVehicleScreen(
|
||||
carContext: CarContext,
|
||||
val integrationRepository: IntegrationRepository,
|
||||
private val integrationRepository: IntegrationRepository,
|
||||
private val authenticationRepository: AuthenticationRepository,
|
||||
private val allEntities: Flow<Map<String, Entity<*>>>
|
||||
) : Screen(carContext) {
|
||||
|
||||
companion object {
|
||||
|
@ -39,25 +60,64 @@ class MainVehicleScreen(
|
|||
"switch" to commonR.string.switches,
|
||||
)
|
||||
private val SUPPORTED_DOMAINS = SUPPORTED_DOMAINS_WITH_STRING.keys
|
||||
|
||||
private val MAP_DOMAINS = listOf(
|
||||
"device_tracker",
|
||||
"person",
|
||||
"zone",
|
||||
)
|
||||
}
|
||||
|
||||
private var isLoggedIn: Boolean? = null
|
||||
private val domains = mutableSetOf<String>()
|
||||
private val entities = mutableMapOf<String, Entity<*>>()
|
||||
|
||||
init {
|
||||
lifecycleScope.launch {
|
||||
integrationRepository.getEntities()?.forEach { entity ->
|
||||
val domain = entity.domain
|
||||
if (domain in SUPPORTED_DOMAINS) {
|
||||
entities[entity.entityId] = entity
|
||||
domains.add(domain)
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
isLoggedIn = authenticationRepository.getSessionState() == SessionState.CONNECTED
|
||||
invalidate()
|
||||
while (isLoggedIn != true) {
|
||||
delay(1000)
|
||||
isLoggedIn =
|
||||
authenticationRepository.getSessionState() == SessionState.CONNECTED
|
||||
invalidate()
|
||||
}
|
||||
allEntities.collect { entities ->
|
||||
domains.clear()
|
||||
entities.values.forEach {
|
||||
if (it.domain in SUPPORTED_DOMAINS) {
|
||||
domains.add(it.domain)
|
||||
}
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
if (isLoggedIn == false) {
|
||||
return MessageTemplate.Builder(carContext.getString(commonR.string.aa_app_not_logged_in))
|
||||
.setTitle(carContext.getString(commonR.string.app_name))
|
||||
.setHeaderAction(Action.APP_ICON)
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setTitle(carContext.getString(commonR.string.login))
|
||||
.setOnClickListener(
|
||||
ParkedOnlyOnClickListener.create {
|
||||
Log.i(TAG, "Starting login activity")
|
||||
carContext.startActivity(
|
||||
Intent(carContext, LaunchActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
val listBuilder = ItemList.Builder()
|
||||
domains.forEach { domain ->
|
||||
val friendlyDomain =
|
||||
|
@ -67,17 +127,39 @@ class MainVehicleScreen(
|
|||
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
|
||||
}
|
||||
}
|
||||
val icon = Entity(
|
||||
"$domain.ha_android_placeholder",
|
||||
"",
|
||||
mapOf<Any, Any>(),
|
||||
Calendar.getInstance(),
|
||||
Calendar.getInstance(),
|
||||
null
|
||||
).getIcon(carContext)
|
||||
|
||||
listBuilder.addItem(
|
||||
Row.Builder()
|
||||
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 clicked")
|
||||
Log.i(TAG, "Domain:$domain clicked")
|
||||
screenManager.push(
|
||||
EntityGridVehicleScreen(
|
||||
carContext,
|
||||
integrationRepository,
|
||||
friendlyDomain,
|
||||
entities.filter { it.value.domain == domain }.toMutableMap()
|
||||
allEntities.map { it.values.filter { entity -> entity.domain == domain } }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -85,12 +167,43 @@ class MainVehicleScreen(
|
|||
)
|
||||
}
|
||||
|
||||
// TODO: Add row for zones so we can start navigation?
|
||||
listBuilder.addItem(
|
||||
Row.Builder()
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconicsDrawable(
|
||||
carContext,
|
||||
CommunityMaterial.Icon3.cmd_map_outline
|
||||
).apply {
|
||||
sizeDp = 48
|
||||
}.toAndroidIconCompat()
|
||||
)
|
||||
.setTint(CarColor.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
.setTitle(carContext.getString(commonR.string.aa_navigation))
|
||||
.setOnClickListener {
|
||||
Log.i(TAG, "Navigation clicked")
|
||||
screenManager.push(
|
||||
MapVehicleScreen(
|
||||
carContext,
|
||||
integrationRepository,
|
||||
allEntities.map { it.values.filter { entity -> entity.domain in MAP_DOMAINS } }
|
||||
)
|
||||
)
|
||||
}
|
||||
.build()
|
||||
)
|
||||
|
||||
return ListTemplate.Builder()
|
||||
.setTitle(carContext.getString(io.homeassistant.companion.android.common.R.string.app_name))
|
||||
.setHeaderAction(Action.APP_ICON)
|
||||
.setSingleList(listBuilder.build())
|
||||
.build()
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(commonR.string.app_name))
|
||||
setHeaderAction(Action.APP_ICON)
|
||||
if (domains.isEmpty()) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
setLoading(false)
|
||||
setSingleList(listBuilder.build())
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
package io.homeassistant.companion.android.vehicle
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
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.CarColor
|
||||
import androidx.car.app.model.CarIcon
|
||||
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.Lifecycle
|
||||
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.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
|
||||
import io.homeassistant.companion.android.common.data.integration.getIcon
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
class MapVehicleScreen(
|
||||
carContext: CarContext,
|
||||
val integrationRepository: IntegrationRepository,
|
||||
val entitiesFlow: Flow<List<Entity<*>>>,
|
||||
) : Screen(carContext) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MapVehicleScreen"
|
||||
}
|
||||
|
||||
var loading = true
|
||||
var entities: List<Entity<*>> = listOf()
|
||||
|
||||
init {
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
entitiesFlow.collect {
|
||||
loading = false
|
||||
entities = it
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
val listBuilder = ItemList.Builder()
|
||||
entities
|
||||
.mapNotNull {
|
||||
val attrs = it.attributes as? Map<*, *>
|
||||
if (attrs != null) {
|
||||
val lat = attrs["latitude"] as? Double
|
||||
val lon = attrs["longitude"] as? Double
|
||||
if (lat != null && lon != null) {
|
||||
return@mapNotNull Pair(it, listOf(lat, lon))
|
||||
}
|
||||
}
|
||||
return@mapNotNull null
|
||||
}
|
||||
.sortedBy { it.first.friendlyName }
|
||||
.forEach { (entity, location) ->
|
||||
val icon = entity.getIcon(carContext) ?: CommunityMaterial.Icon.cmd_account
|
||||
listBuilder.addItem(
|
||||
Row.Builder()
|
||||
.setTitle(entity.friendlyName)
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconicsDrawable(carContext, icon)
|
||||
.apply {
|
||||
sizeDp = 48
|
||||
}.toAndroidIconCompat()
|
||||
)
|
||||
.setTint(CarColor.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
Log.i(TAG, "${entity.entityId} clicked")
|
||||
val intent = Intent(
|
||||
CarContext.ACTION_NAVIGATE,
|
||||
Uri.parse("geo:${location[0]},${location[1]}")
|
||||
)
|
||||
carContext.startCarApp(intent)
|
||||
}
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.aa_navigation))
|
||||
setHeaderAction(Action.BACK)
|
||||
if (loading) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
setLoading(false)
|
||||
listBuilder.setNoItemsMessage(carContext.getString(commonR.string.aa_no_entities_with_locations))
|
||||
setSingleList(listBuilder.build())
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="aa_app_not_logged_in">Not logged in</string>
|
||||
<string name="aa_navigation">Navigation</string>
|
||||
<string name="aa_no_entities_with_locations">No entities with locations found.</string>
|
||||
<string name="account">Account</string>
|
||||
<string name="action_reply">Reply</string>
|
||||
<string name="activity_intent_error">Unable to send activity intent, please check command format</string>
|
||||
|
|
Loading…
Reference in a new issue