Add settings for wear haptic feedback and toast on entity selection (#1893)

* Add settings for wear haptic feedback and toast on entity selection

* Lint

* Implement review suggestions.

* Add preview functions for easy UI viewing in Android Studio (#1901)

* Add settings for wear haptic feedback and toast on entity selection

* Merge in review fixes

* Lint

* Remove duplicate calls

Co-authored-by: Justin Bassett <bassett.justint@gmail.com>
This commit is contained in:
Daniel Shokouhi 2021-11-11 09:48:11 -08:00 committed by GitHub
parent b060f0c7f4
commit 27627fad49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 293 additions and 64 deletions

View file

@ -34,6 +34,10 @@ interface IntegrationRepository {
suspend fun setWearHomeFavorites(favorites: Set<String>)
suspend fun getWearHomeFavorites(): Set<String>
suspend fun setWearHapticFeedback(enabled: Boolean)
suspend fun getWearHapticFeedback(): Boolean
suspend fun setWearToastConfirmation(enabled: Boolean)
suspend fun getWearToastConfirmation(): Boolean
suspend fun getHomeAssistantVersion(): String

View file

@ -57,6 +57,8 @@ class IntegrationRepositoryImpl @Inject constructor(
private const val PREF_CHECK_SENSOR_REGISTRATION_NEXT = "sensor_reg_last"
private const val PREF_WEAR_HOME_FAVORITES = "wear_home_favorites"
private const val PREF_WEAR_HAPTIC_FEEDBACK = "wear_haptic_feedback"
private const val PREF_WEAR_TOAST_CONFIRMATION = "wear_toast_confirmation"
private const val PREF_HA_VERSION = "ha_version"
private const val PREF_AUTOPLAY_VIDEO = "autoplay_video"
private const val PREF_FULLSCREEN_ENABLED = "fullscreen_enabled"
@ -352,6 +354,22 @@ class IntegrationRepositoryImpl @Inject constructor(
return localStorage.getStringSet(PREF_WEAR_HOME_FAVORITES) ?: setOf()
}
override suspend fun setWearHapticFeedback(enabled: Boolean) {
localStorage.putBoolean(PREF_WEAR_HAPTIC_FEEDBACK, enabled)
}
override suspend fun getWearHapticFeedback(): Boolean {
return localStorage.getBoolean(PREF_WEAR_HAPTIC_FEEDBACK)
}
override suspend fun setWearToastConfirmation(enabled: Boolean) {
localStorage.putBoolean(PREF_WEAR_TOAST_CONFIRMATION, enabled)
}
override suspend fun getWearToastConfirmation(): Boolean {
return localStorage.getBoolean(PREF_WEAR_TOAST_CONFIRMATION)
}
override suspend fun getNotificationRateLimits(): RateLimitResponse {
val pushToken = localStorage.getString(PREF_PUSH_TOKEN) ?: ""
val requestBody = RateLimitRequest(pushToken)

View file

@ -11,4 +11,9 @@ interface HomePresenter {
suspend fun getEntities(): List<Entity<*>>
suspend fun getWearHomeFavorites(): List<String>
suspend fun setWearHomeFavorites(favorites: List<String>)
suspend fun getWearHapticFeedback(): Boolean
suspend fun setWearHapticFeedback(enabled: Boolean)
suspend fun getWearToastConfirmation(): Boolean
suspend fun setWearToastConfirmation(enabled: Boolean)
}

View file

@ -106,4 +106,20 @@ class HomePresenterImpl @Inject constructor(
override suspend fun setWearHomeFavorites(favorites: List<String>) {
integrationUseCase.setWearHomeFavorites(favorites.toSet())
}
override suspend fun getWearHapticFeedback(): Boolean {
return integrationUseCase.getWearHapticFeedback()
}
override suspend fun setWearHapticFeedback(enabled: Boolean) {
integrationUseCase.setWearHapticFeedback(enabled)
}
override suspend fun getWearToastConfirmation(): Boolean {
return integrationUseCase.getWearToastConfirmation()
}
override suspend fun setWearToastConfirmation(enabled: Boolean) {
integrationUseCase.setWearToastConfirmation(enabled)
}
}

View file

@ -1,6 +1,7 @@
package io.homeassistant.companion.android.home
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.homeassistant.companion.android.common.data.integration.Entity
@ -20,10 +21,16 @@ class MainViewModel : ViewModel() {
private set
var favoriteEntityIds = mutableStateListOf<String>()
private set
var isHapticEnabled = mutableStateOf(false)
private set
var isToastEnabled = mutableStateOf(false)
private set
fun loadEntities() {
private fun loadEntities() {
viewModelScope.launch {
favoriteEntityIds.addAll(homePresenter.getWearHomeFavorites())
isHapticEnabled.value = homePresenter.getWearHapticFeedback()
isToastEnabled.value = homePresenter.getWearToastConfirmation()
entities.addAll(homePresenter.getEntities())
}
}
@ -62,6 +69,20 @@ class MainViewModel : ViewModel() {
}
}
fun setHapticEnabled(enabled: Boolean) {
viewModelScope.launch {
homePresenter.setWearHapticFeedback(enabled)
isHapticEnabled.value = enabled
}
}
fun setToastEnabled(enabled: Boolean) {
viewModelScope.launch {
homePresenter.setWearToastConfirmation(enabled)
isToastEnabled.value = enabled
}
}
fun logout() {
homePresenter.onLogoutClicked()
}

View file

@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@ -21,6 +22,7 @@ import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.home.HomePresenterImpl
import io.homeassistant.companion.android.util.getIcon
import io.homeassistant.companion.android.util.onEntityClickedFeedback
import io.homeassistant.companion.android.util.previewEntity1
import io.homeassistant.companion.android.util.previewEntity2
import io.homeassistant.companion.android.util.setChipDefaults
@ -28,22 +30,30 @@ import io.homeassistant.companion.android.util.setChipDefaults
@Composable
fun EntityUi(
entity: Entity<*>,
onEntityClicked: (String) -> Unit
onEntityClicked: (String) -> Unit,
isHapticEnabled: Boolean,
isToastEnabled: Boolean
) {
val haptic = LocalHapticFeedback.current
val context = LocalContext.current
val attributes = entity.attributes as Map<*, *>
val iconBitmap = getIcon(attributes["icon"] as String?, entity.entityId.split(".")[0], LocalContext.current)
val friendlyName = attributes["friendly_name"].toString()
if (entity.entityId.split(".")[0] in HomePresenterImpl.toggleDomains) {
ToggleChip(
checked = entity.state == "on",
onCheckedChange = { onEntityClicked(entity.entityId) },
onCheckedChange = {
onEntityClicked(entity.entityId)
onEntityClickedFeedback(isToastEnabled, isHapticEnabled, context, friendlyName, haptic)
},
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp),
appIcon = { Image(asset = iconBitmap ?: CommunityMaterial.Icon.cmd_cellphone) },
label = {
Text(
text = attributes["friendly_name"].toString(),
text = friendlyName,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
@ -69,13 +79,16 @@ fun EntityUi(
icon = { Image(asset = iconBitmap ?: CommunityMaterial.Icon.cmd_cellphone) },
label = {
Text(
text = attributes["friendly_name"].toString(),
text = friendlyName,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
},
enabled = entity.state != "unavailable",
onClick = { onEntityClicked(entity.entityId) },
onClick = {
onEntityClicked(entity.entityId)
onEntityClickedFeedback(isToastEnabled, isHapticEnabled, context, friendlyName, haptic)
},
colors = setChipDefaults()
)
}
@ -87,11 +100,15 @@ private fun PreviewEntityUI() {
Column {
EntityUi(
entity = previewEntity1,
onEntityClicked = {}
onEntityClicked = {},
isHapticEnabled = true,
isToastEnabled = false
)
EntityUi(
entity = previewEntity2,
onEntityClicked = {}
onEntityClicked = {},
isHapticEnabled = false,
isToastEnabled = true
)
}
}

View file

@ -68,14 +68,20 @@ fun LoadHomePage(
mainViewModel.favoriteEntityIds,
{ mainViewModel.toggleEntity(it) },
{ swipeDismissableNavController.navigate(SCREEN_SETTINGS) },
{ mainViewModel.logout() }
{ mainViewModel.logout() },
mainViewModel.isHapticEnabled.value,
mainViewModel.isToastEnabled.value
)
}
composable(SCREEN_SETTINGS) {
SettingsView(
mainViewModel.favoriteEntityIds,
{ swipeDismissableNavController.navigate(SCREEN_SET_FAVORITES) },
{ mainViewModel.clearFavorites() }
{ mainViewModel.clearFavorites() },
mainViewModel.isHapticEnabled.value,
mainViewModel.isToastEnabled.value,
{ mainViewModel.setHapticEnabled(it) },
{ mainViewModel.setToastEnabled(it) }
)
}
composable(SCREEN_SET_FAVORITES) {

View file

@ -14,6 +14,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@ -37,6 +39,7 @@ import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.util.LocalRotaryEventDispatcher
import io.homeassistant.companion.android.util.RotaryEventDispatcher
import io.homeassistant.companion.android.util.RotaryEventState
import io.homeassistant.companion.android.util.onEntityClickedFeedback
import io.homeassistant.companion.android.util.previewEntityList
import io.homeassistant.companion.android.util.previewFavoritesList
import io.homeassistant.companion.android.util.setChipDefaults
@ -48,7 +51,9 @@ fun MainView(
favoriteEntityIds: List<String>,
onEntityClicked: (String) -> Unit,
onSettingsClicked: () -> Unit,
onLogoutClicked: () -> Unit
onLogoutClicked: () -> Unit,
isHapticEnabled: Boolean,
isToastEnabled: Boolean
) {
val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState()
@ -67,6 +72,8 @@ fun MainView(
val inputBooleans = entities.filter { it.entityId.split(".")[0] == "input_boolean" }
val switches = entities.filter { it.entityId.split(".")[0] == "switch" }
val haptic = LocalHapticFeedback.current
val context = LocalContext.current
RotaryEventDispatcher(scalingLazyListState)
RotaryEventState(scrollState = scalingLazyListState)
@ -121,7 +128,10 @@ fun MainView(
overflow = TextOverflow.Ellipsis
)
},
onClick = { onEntityClicked(favoriteEntityID) },
onClick = {
onEntityClicked(favoriteEntityID)
onEntityClickedFeedback(isToastEnabled, isHapticEnabled, context, favoriteEntityID, haptic)
},
colors = ChipDefaults.primaryChipColors(
backgroundColor = colorResource(id = R.color.colorAccent),
contentColor = Color.Black
@ -130,7 +140,9 @@ fun MainView(
} else {
EntityUi(
entityMap[favoriteEntityID]!!,
onEntityClicked
onEntityClicked,
isHapticEnabled,
isToastEnabled
)
}
}
@ -169,7 +181,7 @@ fun MainView(
}
if (expandedInputBooleans) {
items(inputBooleans.size) { index ->
EntityUi(inputBooleans[index], onEntityClicked)
EntityUi(inputBooleans[index], onEntityClicked, isHapticEnabled, isToastEnabled)
}
}
}
@ -183,7 +195,7 @@ fun MainView(
}
if (expandedLights) {
items(lights.size) { index ->
EntityUi(lights[index], onEntityClicked)
EntityUi(lights[index], onEntityClicked, isHapticEnabled, isToastEnabled)
}
}
}
@ -197,7 +209,7 @@ fun MainView(
}
if (expandedScenes) {
items(scenes.size) { index ->
EntityUi(scenes[index], onEntityClicked)
EntityUi(scenes[index], onEntityClicked, isHapticEnabled, isToastEnabled)
}
}
}
@ -211,7 +223,7 @@ fun MainView(
}
if (expandedScripts) {
items(scripts.size) { index ->
EntityUi(scripts[index], onEntityClicked)
EntityUi(scripts[index], onEntityClicked, isHapticEnabled, isToastEnabled)
}
}
}
@ -225,7 +237,7 @@ fun MainView(
}
if (expandedSwitches) {
items(switches.size) { index ->
EntityUi(switches[index], onEntityClicked)
EntityUi(switches[index], onEntityClicked, isHapticEnabled, isToastEnabled)
}
}
}
@ -252,7 +264,10 @@ private fun PreviewMainView() {
entities = previewEntityList,
favoriteEntityIds = previewFavoritesList,
onEntityClicked = {},
onSettingsClicked = {}
) {}
onSettingsClicked = {},
onLogoutClicked = {},
isHapticEnabled = true,
isToastEnabled = false
)
}
}

View file

@ -1,78 +1,192 @@
package io.homeassistant.companion.android.home.views
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
import androidx.wear.compose.material.ScalingLazyColumn
import androidx.wear.compose.material.ScalingLazyListState
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.ToggleChip
import androidx.wear.compose.material.ToggleChipDefaults
import androidx.wear.compose.material.rememberScalingLazyListState
import com.mikepenz.iconics.compose.Image
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.util.LocalRotaryEventDispatcher
import io.homeassistant.companion.android.util.RotaryEventDispatcher
import io.homeassistant.companion.android.util.RotaryEventState
import io.homeassistant.companion.android.util.previewFavoritesList
@Composable
fun SettingsView(
favorites: List<String>,
onClickSetFavorites: () -> Unit,
onClearFavorites: () -> Unit
onClearFavorites: () -> Unit,
isHapticEnabled: Boolean,
isToastEnabled: Boolean,
onHapticEnabled: (Boolean) -> Unit,
onToastEnabled: (Boolean) -> Unit
) {
Column {
ListHeader(id = R.string.settings)
Chip(
modifier = Modifier
.fillMaxWidth(),
icon = {
Image(asset = CommunityMaterial.Icon3.cmd_star)
},
label = {
Text(
text = stringResource(id = R.string.favorite)
val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState()
RotaryEventState(scrollState = scalingLazyListState)
ScalingLazyColumn(
modifier = Modifier
.fillMaxSize(),
contentPadding = PaddingValues(
top = 10.dp,
start = 5.dp,
end = 5.dp,
bottom = 40.dp
),
horizontalAlignment = Alignment.CenterHorizontally,
state = scalingLazyListState
) {
item {
ListHeader(id = R.string.settings)
}
item {
Chip(
modifier = Modifier
.fillMaxWidth()
.padding(top = 20.dp),
icon = {
Image(asset = CommunityMaterial.Icon3.cmd_star)
},
label = {
Text(
text = stringResource(id = R.string.favorite)
)
},
onClick = onClickSetFavorites,
colors = ChipDefaults.primaryChipColors(
contentColor = Color.Black
)
},
onClick = onClickSetFavorites,
colors = ChipDefaults.primaryChipColors(
contentColor = Color.Black
)
)
Chip(
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp),
icon = {
Image(asset = CommunityMaterial.Icon.cmd_delete)
},
label = {
Text(
text = stringResource(id = R.string.clear_favorites),
}
item {
Chip(
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp),
icon = {
Image(asset = CommunityMaterial.Icon.cmd_delete)
},
label = {
Text(
text = stringResource(id = R.string.clear_favorites),
)
},
onClick = onClearFavorites,
colors = ChipDefaults.primaryChipColors(
contentColor = Color.Black
),
secondaryLabel = {
Text(
text = stringResource(id = R.string.irreverisble)
)
},
enabled = favorites.isNotEmpty()
)
}
item {
val haptic = LocalHapticFeedback.current
ToggleChip(
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp),
checked = isHapticEnabled,
onCheckedChange = {
onHapticEnabled(it)
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
},
label = {
Text(stringResource(R.string.setting_haptic_label))
},
appIcon = {
Image(
asset =
if (isHapticEnabled)
CommunityMaterial.Icon3.cmd_watch_vibrate
else
CommunityMaterial.Icon3.cmd_watch_vibrate_off
)
},
colors = ToggleChipDefaults.toggleChipColors(
checkedStartBackgroundColor = Color(0xFFAECBFA),
checkedEndBackgroundColor = Color(0xFFAECBFA),
uncheckedStartBackgroundColor = Color(0xFFAECBFA),
uncheckedEndBackgroundColor = Color(0xFFAECBFA),
checkedContentColor = Color.Black,
uncheckedContentColor = Color.Black,
)
},
onClick = onClearFavorites,
colors = ChipDefaults.primaryChipColors(
contentColor = Color.Black
),
secondaryLabel = {
Text(
text = stringResource(id = R.string.irreverisble)
)
}
item {
ToggleChip(
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp),
checked = isToastEnabled,
onCheckedChange = {
onToastEnabled(it)
},
label = {
Text(stringResource(R.string.setting_toast_label))
},
appIcon = {
Image(
asset =
if (isToastEnabled)
CommunityMaterial.Icon3.cmd_message
else
CommunityMaterial.Icon3.cmd_message_off
)
},
colors = ToggleChipDefaults.toggleChipColors(
checkedStartBackgroundColor = Color(0xFFAECBFA),
checkedEndBackgroundColor = Color(0xFFAECBFA),
uncheckedStartBackgroundColor = Color(0xFFAECBFA),
uncheckedEndBackgroundColor = Color(0xFFAECBFA),
checkedContentColor = Color.Black,
uncheckedContentColor = Color.Black,
)
},
enabled = favorites.isNotEmpty()
)
)
}
}
}
@Preview
@Composable
private fun PreviewSettingsView() {
SettingsView(
favorites = previewFavoritesList,
onClickSetFavorites = { /*TODO*/ },
onClearFavorites = {}
)
val rotaryEventDispatcher = RotaryEventDispatcher()
CompositionLocalProvider(
LocalRotaryEventDispatcher provides rotaryEventDispatcher
) {
SettingsView(
favorites = previewFavoritesList,
onClickSetFavorites = { /*TODO*/ },
onClearFavorites = {},
isHapticEnabled = true,
isToastEnabled = false,
{},
{}
)
}
}

View file

@ -1,8 +1,11 @@
package io.homeassistant.companion.android.util
import android.content.Context
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.res.colorResource
import androidx.wear.compose.material.ChipColors
import androidx.wear.compose.material.ChipDefaults
@ -33,3 +36,10 @@ fun getIcon(icon: String?, domain: String, context: Context): IIcon? {
}
}
}
fun onEntityClickedFeedback(isToastEnabled: Boolean, isHapticEnabled: Boolean, context: Context, friendlyName: String, haptic: HapticFeedback) {
if (isToastEnabled)
Toast.makeText(context, context.getString(R.string.toast_message, friendlyName), Toast.LENGTH_SHORT).show()
if (isHapticEnabled)
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}

View file

@ -40,4 +40,7 @@
<string name="set_favorite">Set Favorites</string>
<string name="clear_favorites">Clear Favorites</string>
<string name="irreverisble">This action is irreversible</string>
<string name="toast_message">%1$s was selected</string>
<string name="setting_toast_label">Enable Toast Confirmation</string>
<string name="setting_haptic_label">Enable Haptic Feeback</string>
</resources>