mirror of
https://github.com/home-assistant/android
synced 2024-10-04 23:29:31 +00:00
Share Wear session with main app to fix mismatch (#3439)
- Exchange server information between the Wear app and phone app, and create a temporary server on the phone that holds the Wear server information, to ensure that the same server is used on both devices
This commit is contained in:
parent
4aadcd0315
commit
b6688c6388
|
@ -1,5 +1,6 @@
|
|||
package io.homeassistant.companion.android.settings.wear
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
|
@ -23,6 +24,15 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import io.homeassistant.companion.android.HomeAssistantApplication
|
||||
import io.homeassistant.companion.android.common.data.integration.Entity
|
||||
import io.homeassistant.companion.android.common.data.servers.ServerManager
|
||||
import io.homeassistant.companion.android.common.util.WearDataMessages
|
||||
import io.homeassistant.companion.android.database.server.Server
|
||||
import io.homeassistant.companion.android.database.server.ServerConnectionInfo
|
||||
import io.homeassistant.companion.android.database.server.ServerSessionInfo
|
||||
import io.homeassistant.companion.android.database.server.ServerType
|
||||
import io.homeassistant.companion.android.database.server.ServerUserInfo
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -32,6 +42,7 @@ import javax.inject.Inject
|
|||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
@HiltViewModel
|
||||
@SuppressLint("VisibleForTests") // https://issuetracker.google.com/issues/239451111
|
||||
class SettingsWearViewModel @Inject constructor(
|
||||
private val serverManager: ServerManager,
|
||||
application: Application
|
||||
|
@ -42,13 +53,6 @@ class SettingsWearViewModel @Inject constructor(
|
|||
companion object {
|
||||
private const val TAG = "SettingsWearViewModel"
|
||||
private const val CAPABILITY_WEAR_SENDS_CONFIG = "sends_config"
|
||||
|
||||
private const val KEY_UPDATE_TIME = "UpdateTime"
|
||||
private const val KEY_IS_AUTHENTICATED = "isAuthenticated"
|
||||
private const val KEY_SUPPORTED_DOMAINS = "supportedDomains"
|
||||
private const val KEY_FAVORITES = "favorites"
|
||||
private const val KEY_TEMPLATE_TILE = "templateTile"
|
||||
private const val KEY_TEMPLATE_TILE_REFRESH_INTERVAL = "templateTileRefreshInterval"
|
||||
}
|
||||
|
||||
private val objectMapper = jacksonObjectMapper()
|
||||
|
@ -57,6 +61,9 @@ class SettingsWearViewModel @Inject constructor(
|
|||
val hasData = _hasData.asStateFlow()
|
||||
private val _isAuthenticated = MutableStateFlow(false)
|
||||
val isAuthenticated = _isAuthenticated.asStateFlow()
|
||||
private var serverId = 0
|
||||
private var remoteServerId = 0
|
||||
|
||||
var entities = mutableStateMapOf<String, Entity<*>>()
|
||||
private set
|
||||
var supportedDomains = mutableStateListOf<String>()
|
||||
|
@ -101,22 +108,34 @@ class SettingsWearViewModel @Inject constructor(
|
|||
).show()
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
if (serverManager.isRegistered()) {
|
||||
serverManager.integrationRepository().getEntities()?.forEach {
|
||||
entities[it.entityId] = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
Wearable.getDataClient(getApplication<HomeAssistantApplication>()).removeListener(this)
|
||||
|
||||
if (serverId != 0) {
|
||||
CoroutineScope(Dispatchers.Main + Job()).launch {
|
||||
serverManager.removeServer(serverId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadEntities() {
|
||||
if (serverId != 0) {
|
||||
try {
|
||||
serverManager.integrationRepository(serverId).getEntities()?.forEach {
|
||||
entities[it.entityId] = it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to load entities for Wear server", e)
|
||||
entities.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setTemplateContent(template: String) {
|
||||
templateTileContent.value = template
|
||||
if (template.isNotEmpty()) {
|
||||
if (template.isNotEmpty() && serverId != 0) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
templateTileContentRendered.value =
|
||||
|
@ -161,8 +180,8 @@ class SettingsWearViewModel @Inject constructor(
|
|||
fun sendHomeFavorites(favoritesList: List<String>) = viewModelScope.launch {
|
||||
val application = getApplication<HomeAssistantApplication>()
|
||||
val putDataRequest = PutDataMapRequest.create("/updateFavorites").run {
|
||||
dataMap.putLong(KEY_UPDATE_TIME, System.nanoTime())
|
||||
dataMap.putString(KEY_FAVORITES, objectMapper.writeValueAsString(favoritesList))
|
||||
dataMap.putLong(WearDataMessages.KEY_UPDATE_TIME, System.nanoTime())
|
||||
dataMap.putString(WearDataMessages.CONFIG_FAVORITES, objectMapper.writeValueAsString(favoritesList))
|
||||
setUrgent()
|
||||
asPutDataRequest()
|
||||
}
|
||||
|
@ -205,8 +224,8 @@ class SettingsWearViewModel @Inject constructor(
|
|||
|
||||
fun sendTemplateTileInfo() {
|
||||
val putDataRequest = PutDataMapRequest.create("/updateTemplateTile").run {
|
||||
dataMap.putString(KEY_TEMPLATE_TILE, templateTileContent.value)
|
||||
dataMap.putInt(KEY_TEMPLATE_TILE_REFRESH_INTERVAL, templateTileRefreshInterval.value)
|
||||
dataMap.putString(WearDataMessages.CONFIG_TEMPLATE_TILE, templateTileContent.value)
|
||||
dataMap.putInt(WearDataMessages.CONFIG_TEMPLATE_TILE_REFRESH_INTERVAL, templateTileRefreshInterval.value)
|
||||
setUrgent()
|
||||
asPutDataRequest()
|
||||
}
|
||||
|
@ -233,20 +252,73 @@ class SettingsWearViewModel @Inject constructor(
|
|||
dataEvents.release()
|
||||
}
|
||||
|
||||
private fun onLoadConfigFromWear(data: DataMap) {
|
||||
_isAuthenticated.value = data.getBoolean(KEY_IS_AUTHENTICATED, false)
|
||||
private fun onLoadConfigFromWear(data: DataMap) = viewModelScope.launch {
|
||||
val isAuthenticated = data.getBoolean(WearDataMessages.CONFIG_IS_AUTHENTICATED, false)
|
||||
_isAuthenticated.value = isAuthenticated
|
||||
if (isAuthenticated) {
|
||||
updateServer(data)
|
||||
}
|
||||
|
||||
val supportedDomainsList: List<String> =
|
||||
objectMapper.readValue(data.getString(KEY_SUPPORTED_DOMAINS, "[\"input_boolean\", \"light\", \"lock\", \"switch\", \"script\", \"scene\"]"))
|
||||
objectMapper.readValue(data.getString(WearDataMessages.CONFIG_SUPPORTED_DOMAINS, "[\"input_boolean\", \"light\", \"lock\", \"switch\", \"script\", \"scene\"]"))
|
||||
supportedDomains.clear()
|
||||
supportedDomains.addAll(supportedDomainsList)
|
||||
val favoriteEntityIdList: List<String> =
|
||||
objectMapper.readValue(data.getString(KEY_FAVORITES, "[]"))
|
||||
objectMapper.readValue(data.getString(WearDataMessages.CONFIG_FAVORITES, "[]"))
|
||||
favoriteEntityIds.clear()
|
||||
favoriteEntityIdList.forEach { entityId ->
|
||||
favoriteEntityIds.add(entityId)
|
||||
}
|
||||
setTemplateContent(data.getString(KEY_TEMPLATE_TILE, ""))
|
||||
templateTileRefreshInterval.value = data.getInt(KEY_TEMPLATE_TILE_REFRESH_INTERVAL, 0)
|
||||
setTemplateContent(data.getString(WearDataMessages.CONFIG_TEMPLATE_TILE, ""))
|
||||
templateTileRefreshInterval.value = data.getInt(WearDataMessages.CONFIG_TEMPLATE_TILE_REFRESH_INTERVAL, 0)
|
||||
|
||||
_hasData.value = true
|
||||
}
|
||||
|
||||
private suspend fun updateServer(data: DataMap) {
|
||||
val wearServerId = data.getInt(WearDataMessages.CONFIG_SERVER_ID, 0)
|
||||
if (wearServerId == 0 || wearServerId == remoteServerId) return
|
||||
|
||||
if (remoteServerId != 0) { // First, remove the old server
|
||||
serverManager.removeServer(serverId)
|
||||
serverId = 0
|
||||
remoteServerId = 0
|
||||
}
|
||||
|
||||
val wearExternalUrl = data.getString(WearDataMessages.CONFIG_SERVER_EXTERNAL_URL) ?: return
|
||||
val wearWebhookId = data.getString(WearDataMessages.CONFIG_SERVER_WEBHOOK_ID) ?: return
|
||||
val wearCloudUrl = data.getString(WearDataMessages.CONFIG_SERVER_CLOUD_URL, "").ifBlank { null }
|
||||
val wearCloudhookUrl = data.getString(WearDataMessages.CONFIG_SERVER_CLOUDHOOK_URL, "").ifBlank { null }
|
||||
val wearUseCloud = data.getBoolean(WearDataMessages.CONFIG_SERVER_USE_CLOUD, false)
|
||||
val wearRefreshToken = data.getString(WearDataMessages.CONFIG_SERVER_REFRESH_TOKEN, "")
|
||||
|
||||
try {
|
||||
serverId = serverManager.addServer(
|
||||
Server(
|
||||
_name = "",
|
||||
type = ServerType.TEMPORARY,
|
||||
connection = ServerConnectionInfo(
|
||||
externalUrl = wearExternalUrl,
|
||||
cloudUrl = wearCloudUrl,
|
||||
webhookId = wearWebhookId,
|
||||
cloudhookUrl = wearCloudhookUrl,
|
||||
useCloud = wearUseCloud
|
||||
),
|
||||
session = ServerSessionInfo(),
|
||||
user = ServerUserInfo()
|
||||
)
|
||||
)
|
||||
serverManager.authenticationRepository(serverId).registerRefreshToken(wearRefreshToken)
|
||||
remoteServerId = wearServerId
|
||||
|
||||
viewModelScope.launch { loadEntities() }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to add Wear server from data", e)
|
||||
if (serverId != 0) {
|
||||
serverManager.removeServer(serverId)
|
||||
serverId = 0
|
||||
remoteServerId = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import io.homeassistant.companion.android.common.data.authentication.impl.Authen
|
|||
interface AuthenticationRepository {
|
||||
|
||||
suspend fun registerAuthorizationCode(authorizationCode: String)
|
||||
suspend fun registerRefreshToken(refreshToken: String)
|
||||
|
||||
suspend fun retrieveExternalAuthentication(forceRefresh: Boolean): String
|
||||
|
||||
|
|
|
@ -55,6 +55,15 @@ class AuthenticationRepositoryImpl @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun registerRefreshToken(refreshToken: String) {
|
||||
val url = server.connection.getUrl()?.toHttpUrlOrNull()
|
||||
if (url == null) {
|
||||
Log.e(TAG, "Unable to register session with refresh token.")
|
||||
return
|
||||
}
|
||||
refreshSessionWithToken(refreshToken)
|
||||
}
|
||||
|
||||
override suspend fun retrieveExternalAuthentication(forceRefresh: Boolean): String {
|
||||
ensureValidSession(forceRefresh)
|
||||
return jacksonObjectMapper().writeValueAsString(
|
||||
|
@ -133,33 +142,37 @@ class AuthenticationRepositoryImpl @AssistedInject constructor(
|
|||
}
|
||||
|
||||
if (server.session.isExpired() || forceRefresh) {
|
||||
return authenticationService.refreshToken(
|
||||
url.newBuilder().addPathSegments("auth/token").build(),
|
||||
AuthenticationService.GRANT_TYPE_REFRESH,
|
||||
server.session.refreshToken!!,
|
||||
AuthenticationService.CLIENT_ID
|
||||
).let {
|
||||
if (it.isSuccessful) {
|
||||
val refreshedToken = it.body() ?: throw AuthorizationException()
|
||||
serverManager.updateServer(
|
||||
server.copy(
|
||||
session = ServerSessionInfo(
|
||||
refreshedToken.accessToken,
|
||||
server.session.refreshToken,
|
||||
System.currentTimeMillis() / 1000 + refreshedToken.expiresIn,
|
||||
refreshedToken.tokenType,
|
||||
installId
|
||||
)
|
||||
refreshSessionWithToken(server.session.refreshToken!!)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshSessionWithToken(refreshToken: String) {
|
||||
return authenticationService.refreshToken(
|
||||
server.connection.getUrl()?.toHttpUrlOrNull()!!.newBuilder().addPathSegments("auth/token").build(),
|
||||
AuthenticationService.GRANT_TYPE_REFRESH,
|
||||
refreshToken,
|
||||
AuthenticationService.CLIENT_ID
|
||||
).let {
|
||||
if (it.isSuccessful) {
|
||||
val refreshedToken = it.body() ?: throw AuthorizationException()
|
||||
serverManager.updateServer(
|
||||
server.copy(
|
||||
session = ServerSessionInfo(
|
||||
refreshedToken.accessToken,
|
||||
refreshToken,
|
||||
System.currentTimeMillis() / 1000 + refreshedToken.expiresIn,
|
||||
refreshedToken.tokenType,
|
||||
installId
|
||||
)
|
||||
)
|
||||
return@let
|
||||
} else if (it.code() == 400 &&
|
||||
it.errorBody()?.string()?.contains("invalid_grant") == true
|
||||
) {
|
||||
revokeSession()
|
||||
}
|
||||
throw AuthorizationException()
|
||||
)
|
||||
return@let
|
||||
} else if (it.code() == 400 &&
|
||||
it.errorBody()?.string()?.contains("invalid_grant") == true
|
||||
) {
|
||||
revokeSession()
|
||||
}
|
||||
throw AuthorizationException()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package io.homeassistant.companion.android.common.util
|
||||
|
||||
object WearDataMessages {
|
||||
const val KEY_UPDATE_TIME = "UpdateTime"
|
||||
|
||||
const val CONFIG_IS_AUTHENTICATED = "isAuthenticated"
|
||||
const val CONFIG_SERVER_ID = "serverId"
|
||||
const val CONFIG_SERVER_EXTERNAL_URL = "serverExternalUrl"
|
||||
const val CONFIG_SERVER_WEBHOOK_ID = "serverWebhookId"
|
||||
const val CONFIG_SERVER_CLOUD_URL = "serverCloudUrl"
|
||||
const val CONFIG_SERVER_CLOUDHOOK_URL = "serverCloudhookUrl"
|
||||
const val CONFIG_SERVER_USE_CLOUD = "serverUseCloud"
|
||||
const val CONFIG_SERVER_REFRESH_TOKEN = "serverRefreshToken"
|
||||
const val CONFIG_SUPPORTED_DOMAINS = "supportedDomains"
|
||||
const val CONFIG_FAVORITES = "favorites"
|
||||
const val CONFIG_TEMPLATE_TILE = "templateTile"
|
||||
const val CONFIG_TEMPLATE_TILE_REFRESH_INTERVAL = "templateTileRefreshInterval"
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package io.homeassistant.companion.android.phone
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
|
@ -18,6 +19,7 @@ import io.homeassistant.companion.android.BuildConfig
|
|||
import io.homeassistant.companion.android.common.data.integration.DeviceRegistration
|
||||
import io.homeassistant.companion.android.common.data.prefs.WearPrefsRepository
|
||||
import io.homeassistant.companion.android.common.data.servers.ServerManager
|
||||
import io.homeassistant.companion.android.common.util.WearDataMessages
|
||||
import io.homeassistant.companion.android.database.server.Server
|
||||
import io.homeassistant.companion.android.database.server.ServerConnectionInfo
|
||||
import io.homeassistant.companion.android.database.server.ServerSessionInfo
|
||||
|
@ -38,6 +40,7 @@ import kotlinx.coroutines.tasks.await
|
|||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@SuppressLint("VisibleForTests") // https://issuetracker.google.com/issues/239451111
|
||||
class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChangedListener {
|
||||
|
||||
@Inject
|
||||
|
@ -55,13 +58,6 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
|
|||
|
||||
companion object {
|
||||
private const val TAG = "PhoneSettingsListener"
|
||||
|
||||
private const val KEY_UPDATE_TIME = "UpdateTime"
|
||||
private const val KEY_IS_AUTHENTICATED = "isAuthenticated"
|
||||
private const val KEY_SUPPORTED_DOMAINS = "supportedDomains"
|
||||
private const val KEY_FAVORITES = "favorites"
|
||||
private const val KEY_TEMPLATE_TILE = "templateTile"
|
||||
private const val KEY_TEMPLATE_TILE_REFRESH_INTERVAL = "templateTileRefreshInterval"
|
||||
}
|
||||
|
||||
override fun onMessageReceived(event: MessageEvent) {
|
||||
|
@ -74,12 +70,22 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
|
|||
private fun sendPhoneData() = mainScope.launch {
|
||||
val currentFavorites = favoritesDao.getAll()
|
||||
val putDataRequest = PutDataMapRequest.create("/config").run {
|
||||
dataMap.putLong(KEY_UPDATE_TIME, System.nanoTime())
|
||||
dataMap.putBoolean(KEY_IS_AUTHENTICATED, serverManager.isRegistered())
|
||||
dataMap.putString(KEY_SUPPORTED_DOMAINS, objectMapper.writeValueAsString(HomePresenterImpl.supportedDomains))
|
||||
dataMap.putString(KEY_FAVORITES, objectMapper.writeValueAsString(currentFavorites))
|
||||
dataMap.putString(KEY_TEMPLATE_TILE, wearPrefsRepository.getTemplateTileContent())
|
||||
dataMap.putInt(KEY_TEMPLATE_TILE_REFRESH_INTERVAL, wearPrefsRepository.getTemplateTileRefreshInterval())
|
||||
dataMap.putLong(WearDataMessages.KEY_UPDATE_TIME, System.nanoTime())
|
||||
val isRegistered = serverManager.isRegistered()
|
||||
dataMap.putBoolean(WearDataMessages.CONFIG_IS_AUTHENTICATED, isRegistered)
|
||||
if (isRegistered) {
|
||||
dataMap.putInt(WearDataMessages.CONFIG_SERVER_ID, serverManager.getServer()?.id ?: 0)
|
||||
dataMap.putString(WearDataMessages.CONFIG_SERVER_EXTERNAL_URL, serverManager.getServer()?.connection?.externalUrl ?: "")
|
||||
dataMap.putString(WearDataMessages.CONFIG_SERVER_WEBHOOK_ID, serverManager.getServer()?.connection?.webhookId ?: "")
|
||||
dataMap.putString(WearDataMessages.CONFIG_SERVER_CLOUD_URL, serverManager.getServer()?.connection?.cloudUrl ?: "")
|
||||
dataMap.putString(WearDataMessages.CONFIG_SERVER_CLOUDHOOK_URL, serverManager.getServer()?.connection?.cloudhookUrl ?: "")
|
||||
dataMap.putBoolean(WearDataMessages.CONFIG_SERVER_USE_CLOUD, serverManager.getServer()?.connection?.useCloud ?: false)
|
||||
dataMap.putString(WearDataMessages.CONFIG_SERVER_REFRESH_TOKEN, serverManager.getServer()?.session?.refreshToken ?: "")
|
||||
}
|
||||
dataMap.putString(WearDataMessages.CONFIG_SUPPORTED_DOMAINS, objectMapper.writeValueAsString(HomePresenterImpl.supportedDomains))
|
||||
dataMap.putString(WearDataMessages.CONFIG_FAVORITES, objectMapper.writeValueAsString(currentFavorites))
|
||||
dataMap.putString(WearDataMessages.CONFIG_TEMPLATE_TILE, wearPrefsRepository.getTemplateTileContent())
|
||||
dataMap.putInt(WearDataMessages.CONFIG_TEMPLATE_TILE_REFRESH_INTERVAL, wearPrefsRepository.getTemplateTileRefreshInterval())
|
||||
setUrgent()
|
||||
asPutDataRequest()
|
||||
}
|
||||
|
@ -165,7 +171,7 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
|
|||
|
||||
private fun saveFavorites(dataMap: DataMap) {
|
||||
val favoritesIds: List<String> =
|
||||
objectMapper.readValue(dataMap.getString(KEY_FAVORITES, "[]"))
|
||||
objectMapper.readValue(dataMap.getString(WearDataMessages.CONFIG_FAVORITES, "[]"))
|
||||
|
||||
mainScope.launch {
|
||||
favoritesDao.replaceAll(favoritesIds)
|
||||
|
@ -173,8 +179,8 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
|
|||
}
|
||||
|
||||
private fun saveTileTemplate(dataMap: DataMap) = mainScope.launch {
|
||||
val content = dataMap.getString(KEY_TEMPLATE_TILE, "")
|
||||
val interval = dataMap.getInt(KEY_TEMPLATE_TILE_REFRESH_INTERVAL, 0)
|
||||
val content = dataMap.getString(WearDataMessages.CONFIG_TEMPLATE_TILE, "")
|
||||
val interval = dataMap.getInt(WearDataMessages.CONFIG_TEMPLATE_TILE_REFRESH_INTERVAL, 0)
|
||||
wearPrefsRepository.setTemplateTileContent(content)
|
||||
wearPrefsRepository.setTemplateTileRefreshInterval(interval)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue