mirror of
https://github.com/home-assistant/android
synced 2024-10-15 12:32:54 +00:00
Improve phone feedback for Wear onboarding (#3450)
- Show a loading indicator after sending data - Show a Snackbar for success/errors
This commit is contained in:
parent
e592c334ea
commit
75c7c08b2b
|
@ -3,7 +3,6 @@ package io.homeassistant.companion.android.settings.wear
|
|||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
@ -33,11 +32,14 @@ import io.homeassistant.companion.android.database.server.ServerUserInfo
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import org.burnoutcrew.reorderable.ItemPosition
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
|
@ -61,6 +63,7 @@ class SettingsWearViewModel @Inject constructor(
|
|||
val hasData = _hasData.asStateFlow()
|
||||
private val _isAuthenticated = MutableStateFlow(false)
|
||||
val isAuthenticated = _isAuthenticated.asStateFlow()
|
||||
private var authenticateId: String? = null
|
||||
private var serverId = 0
|
||||
private var remoteServerId = 0
|
||||
|
||||
|
@ -77,6 +80,9 @@ class SettingsWearViewModel @Inject constructor(
|
|||
var templateTileRefreshInterval = mutableStateOf(0)
|
||||
private set
|
||||
|
||||
private val _resultSnackbar = MutableSharedFlow<String>()
|
||||
val resultSnackbar = _resultSnackbar.asSharedFlow()
|
||||
|
||||
init {
|
||||
Wearable.getDataClient(application).addListener(this)
|
||||
viewModelScope.launch {
|
||||
|
@ -101,11 +107,7 @@ class SettingsWearViewModel @Inject constructor(
|
|||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to send config request to wear", e)
|
||||
Toast.makeText(
|
||||
application,
|
||||
application.getString(commonR.string.failed_wear_config_request),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
_resultSnackbar.emit(application.getString(commonR.string.failed_watch_connection))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -191,11 +193,7 @@ class SettingsWearViewModel @Inject constructor(
|
|||
Log.d(TAG, "Successfully sent favorites to wear")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to send favorites to wear", e)
|
||||
Toast.makeText(
|
||||
application,
|
||||
application.getString(commonR.string.failure_send_favorites_wear),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
_resultSnackbar.emit(application.getString(commonR.string.failure_send_favorites_wear))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -206,7 +204,10 @@ class SettingsWearViewModel @Inject constructor(
|
|||
deviceTrackingEnabled: Boolean,
|
||||
notificationsEnabled: Boolean
|
||||
) {
|
||||
_hasData.value = false // Show loading indicator
|
||||
val putDataRequest = PutDataMapRequest.create("/authenticate").run {
|
||||
authenticateId = UUID.randomUUID().toString()
|
||||
dataMap.putString("AuthId", authenticateId!!)
|
||||
dataMap.putString("URL", url)
|
||||
dataMap.putString("AuthCode", authCode)
|
||||
dataMap.putString("DeviceName", deviceName)
|
||||
|
@ -216,9 +217,16 @@ class SettingsWearViewModel @Inject constructor(
|
|||
asPutDataRequest()
|
||||
}
|
||||
|
||||
Wearable.getDataClient(getApplication<HomeAssistantApplication>()).putDataItem(putDataRequest).apply {
|
||||
val app = getApplication<HomeAssistantApplication>()
|
||||
Wearable.getDataClient(app).putDataItem(putDataRequest).apply {
|
||||
addOnSuccessListener { Log.d(TAG, "Successfully sent auth to wear") }
|
||||
addOnFailureListener { e -> Log.e(TAG, "Failed to send auth to wear", e) }
|
||||
addOnFailureListener { e ->
|
||||
Log.e(TAG, "Failed to send auth to wear", e)
|
||||
_hasData.value = true
|
||||
viewModelScope.launch {
|
||||
_resultSnackbar.emit(app.getString(commonR.string.failed_watch_connection))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -245,6 +253,9 @@ class SettingsWearViewModel @Inject constructor(
|
|||
"/config" -> {
|
||||
onLoadConfigFromWear(DataMapItem.fromDataItem(item).dataMap)
|
||||
}
|
||||
WearDataMessages.PATH_LOGIN_RESULT -> {
|
||||
onAuthenticateResult(DataMapItem.fromDataItem(item).dataMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -321,4 +332,20 @@ class SettingsWearViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
private fun onAuthenticateResult(data: DataMap) = viewModelScope.launch {
|
||||
val id = data.getString(WearDataMessages.KEY_ID, "")
|
||||
if (id != authenticateId) return@launch
|
||||
|
||||
val success = data.getBoolean(WearDataMessages.KEY_SUCCESS, false)
|
||||
val application = getApplication<HomeAssistantApplication>()
|
||||
if (success) {
|
||||
_resultSnackbar.emit(application.getString(commonR.string.logged_in))
|
||||
} else {
|
||||
val e = data.getString(WearDataMessages.LOGIN_RESULT_EXCEPTION, "")
|
||||
Log.e(TAG, "Watch was unable to register: $e")
|
||||
_resultSnackbar.emit(application.getString(commonR.string.failed_watch_registration))
|
||||
}
|
||||
|
||||
authenticateId = null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,8 +19,10 @@ import androidx.compose.material.MaterialTheme
|
|||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.rememberScaffoldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
|
@ -33,6 +35,9 @@ import com.mikepenz.iconics.typeface.library.community.material.CommunityMateria
|
|||
import io.homeassistant.companion.android.common.data.integration.domain
|
||||
import io.homeassistant.companion.android.settings.wear.SettingsWearViewModel
|
||||
import io.homeassistant.companion.android.util.compose.getEntityDomainString
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.burnoutcrew.reorderable.ReorderableItem
|
||||
import org.burnoutcrew.reorderable.ReorderableLazyListState
|
||||
import org.burnoutcrew.reorderable.detectReorderAfterLongPress
|
||||
|
@ -43,7 +48,8 @@ import io.homeassistant.companion.android.common.R as commonR
|
|||
@Composable
|
||||
fun LoadWearFavoritesSettings(
|
||||
settingsWearViewModel: SettingsWearViewModel,
|
||||
onBackClicked: () -> Unit
|
||||
onBackClicked: () -> Unit,
|
||||
events: SharedFlow<String>
|
||||
) {
|
||||
val reorderState = rememberReorderableLazyListState(
|
||||
onMove = { from, to -> settingsWearViewModel.onMove(from, to) },
|
||||
|
@ -55,7 +61,17 @@ fun LoadWearFavoritesSettings(
|
|||
|
||||
val validEntities = settingsWearViewModel.entities.filter { it.key.split(".")[0] in settingsWearViewModel.supportedDomains }.values.sortedBy { it.entityId }.toList()
|
||||
val favoriteEntities = settingsWearViewModel.favoriteEntityIds
|
||||
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
LaunchedEffect("snackbar") {
|
||||
events.onEach { message ->
|
||||
scaffoldState.snackbarHostState.currentSnackbarData?.dismiss() // in case of rapid-fire events
|
||||
scaffoldState.snackbarHostState.showSnackbar(message)
|
||||
}.launchIn(this)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
scaffoldState = scaffoldState,
|
||||
topBar = {
|
||||
SettingsWearTopAppBar(
|
||||
title = { Text(stringResource(commonR.string.wear_favorite_entities)) },
|
||||
|
|
|
@ -35,7 +35,8 @@ fun LoadSettingsHomeView(
|
|||
composable(SettingsWearMainView.FAVORITES) {
|
||||
LoadWearFavoritesSettings(
|
||||
settingsWearViewModel = settingsWearViewModel,
|
||||
onBackClicked = { navController.navigateUp() }
|
||||
onBackClicked = { navController.navigateUp() },
|
||||
events = settingsWearViewModel.resultSnackbar
|
||||
)
|
||||
}
|
||||
composable(SettingsWearMainView.LANDING) {
|
||||
|
@ -49,7 +50,8 @@ fun LoadSettingsHomeView(
|
|||
navigateFavorites = { navController.navigate(SettingsWearMainView.FAVORITES) },
|
||||
navigateTemplateTile = { navController.navigate(SettingsWearMainView.TEMPLATE) },
|
||||
loginWearOs = loginWearOs,
|
||||
onBackClicked = onStartBackClicked
|
||||
onBackClicked = onStartBackClicked,
|
||||
events = settingsWearViewModel.resultSnackbar
|
||||
)
|
||||
}
|
||||
composable(SettingsWearMainView.TEMPLATE) {
|
||||
|
|
|
@ -10,7 +10,9 @@ import androidx.compose.material.CircularProgressIndicator
|
|||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.rememberScaffoldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
@ -20,6 +22,10 @@ import androidx.compose.ui.unit.dp
|
|||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
import io.homeassistant.companion.android.settings.views.SettingsRow
|
||||
import io.homeassistant.companion.android.util.wearDeviceName
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
@Composable
|
||||
|
@ -30,9 +36,18 @@ fun SettingWearLandingView(
|
|||
navigateFavorites: () -> Unit,
|
||||
navigateTemplateTile: () -> Unit,
|
||||
loginWearOs: () -> Unit,
|
||||
onBackClicked: () -> Unit
|
||||
onBackClicked: () -> Unit,
|
||||
events: Flow<String>
|
||||
) {
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
LaunchedEffect("snackbar") {
|
||||
events.onEach { message ->
|
||||
scaffoldState.snackbarHostState.showSnackbar(message)
|
||||
}.launchIn(this)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
scaffoldState = scaffoldState,
|
||||
topBar = {
|
||||
SettingsWearTopAppBar(
|
||||
title = { Text(stringResource(commonR.string.wear_settings)) },
|
||||
|
@ -104,6 +119,7 @@ private fun PreviewSettingWearLandingView() {
|
|||
navigateFavorites = {},
|
||||
navigateTemplateTile = {},
|
||||
loginWearOs = {},
|
||||
onBackClicked = {}
|
||||
onBackClicked = {},
|
||||
events = emptyFlow()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
package io.homeassistant.companion.android.common.util
|
||||
|
||||
object WearDataMessages {
|
||||
const val PATH_LOGIN_RESULT = "/loginResult"
|
||||
|
||||
const val KEY_ID = "id"
|
||||
const val KEY_SUCCESS = "success"
|
||||
const val KEY_UPDATE_TIME = "UpdateTime"
|
||||
|
||||
const val CONFIG_IS_AUTHENTICATED = "isAuthenticated"
|
||||
|
@ -15,4 +19,6 @@ object WearDataMessages {
|
|||
const val CONFIG_FAVORITES = "favorites"
|
||||
const val CONFIG_TEMPLATE_TILE = "templateTile"
|
||||
const val CONFIG_TEMPLATE_TILE_REFRESH_INTERVAL = "templateTileRefreshInterval"
|
||||
|
||||
const val LOGIN_RESULT_EXCEPTION = "exception"
|
||||
}
|
||||
|
|
|
@ -224,10 +224,12 @@
|
|||
<string name="failed_authentication">Could not authenticate</string>
|
||||
<string name="failed_connection">Could not connect</string>
|
||||
<string name="failed_phone_connection">Could not connect to phone</string>
|
||||
<string name="failed_watch_connection">Could not connect to watch</string>
|
||||
<string name="failed_registration">Could not register</string>
|
||||
<string name="failed_watch_registration">Could not register watch</string>
|
||||
<string name="failed_unsupported">Not supported</string>
|
||||
<string name="failed_scan">Scanning for Home Assistant failed.</string>
|
||||
<string name="failure_send_favorites_wear">Failed to send favorites selection to Wear OS</string>
|
||||
<string name="failure_send_favorites_wear">Failed to send favorites selection to watch</string>
|
||||
<string name="fans">Fans</string>
|
||||
<string name="favorite_settings">Favorite Settings</string>
|
||||
<string name="favorite">Set Favorite Entities</string>
|
||||
|
@ -334,6 +336,7 @@
|
|||
<string name="log_loader_crash">Recent crash</string>
|
||||
<string name="log">Log</string>
|
||||
<string name="logbook">Logbook</string>
|
||||
<string name="logged_in">Logged in</string>
|
||||
<string name="login">Login</string>
|
||||
<string name="login_wear_os_device">Login Wear OS Device</string>
|
||||
<string name="logout">Logout</string>
|
||||
|
@ -926,7 +929,6 @@
|
|||
<string name="widget_text_color_white">White</string>
|
||||
<string name="widgets">Widgets</string>
|
||||
<string name="zone_event_failure">Unable to send zone event to Home Assistant</string>
|
||||
<string name="failed_wear_config_request">Failed to send config request to wear device</string>
|
||||
<string name="companion_app">Companion App</string>
|
||||
<string name="basic_sensor_name_last_used_app">Last Used App</string>
|
||||
<string name="sensor_description_last_used_app">Application name or package name of the last used application on the device</string>
|
||||
|
|
|
@ -125,13 +125,15 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
|
|||
}
|
||||
|
||||
private fun login(dataMap: DataMap) = mainScope.launch {
|
||||
var authId = ""
|
||||
var serverId: Int? = null
|
||||
try {
|
||||
authId = dataMap.getString("AuthId", "")
|
||||
val url = dataMap.getString("URL", "")
|
||||
val authCode = dataMap.getString("AuthCode", "")
|
||||
val deviceName = dataMap.getString("DeviceName")
|
||||
val deviceTrackingEnabled = dataMap.getBoolean("LocationTracking")
|
||||
val notificationsEnabled = dataMap.getString("Notifications")
|
||||
val notificationsEnabled = dataMap.getBoolean("Notifications")
|
||||
|
||||
val formattedUrl = UrlUtil.formattedUrlString(url)
|
||||
val server = Server(
|
||||
|
@ -154,7 +156,10 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
|
|||
)
|
||||
)
|
||||
serverManager.convertTemporaryServer(serverId)
|
||||
updateTiles()
|
||||
launch {
|
||||
sendLoginResult(authId, true, null)
|
||||
updateTiles()
|
||||
}
|
||||
|
||||
val intent = HomeActivity.newInstance(applicationContext)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
|
@ -169,11 +174,32 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
|
|||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Can't revoke session", e)
|
||||
}
|
||||
launch {
|
||||
sendLoginResult(authId, false, e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
|
||||
sendPhoneData()
|
||||
}
|
||||
|
||||
private suspend fun sendLoginResult(id: String?, success: Boolean, exception: String?) {
|
||||
try {
|
||||
val putDataRequest = PutDataMapRequest.create(WearDataMessages.PATH_LOGIN_RESULT).run {
|
||||
dataMap.putString(WearDataMessages.KEY_ID, id ?: "")
|
||||
dataMap.putBoolean(WearDataMessages.KEY_SUCCESS, success)
|
||||
if (exception != null) {
|
||||
dataMap.putString(WearDataMessages.LOGIN_RESULT_EXCEPTION, exception)
|
||||
}
|
||||
setUrgent()
|
||||
asPutDataRequest()
|
||||
}
|
||||
Wearable.getDataClient(this@PhoneSettingsListener).putDataItem(putDataRequest).await()
|
||||
Log.d(TAG, "Successfully sent ${WearDataMessages.PATH_LOGIN_RESULT} to device")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to send ${WearDataMessages.PATH_LOGIN_RESULT} to device", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveFavorites(dataMap: DataMap) {
|
||||
val favoritesIds: List<String> =
|
||||
objectMapper.readValue(dataMap.getString(WearDataMessages.CONFIG_FAVORITES, "[]"))
|
||||
|
|
Loading…
Reference in a new issue