Skip to content

Commit

Permalink
Share Wear session with main app to fix mismatch (#3439)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
jpelgrom committed Apr 1, 2023
1 parent 4aadcd0 commit b6688c6
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 65 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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>()
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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()
}
Expand All @@ -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
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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()
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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) {
Expand All @@ -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()
}
Expand Down Expand Up @@ -165,16 +171,16 @@ 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)
}
}

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)
}
Expand Down

0 comments on commit b6688c6

Please sign in to comment.