Implement support for multiple Shortcut Tiles on Wear OS (#3697)

* Extract JSONArray -> List<String> conversion to extension fun

* Refactor navigation around Shortcuts Tile settings

* Add ShortcutsTileId enum class

* Introduce multiple ShortcutTile subclasses and modify settings UI and storage to support multiple Tiles

* Check if correct ShortcutsTileId is passed as parameter to BaseShortcutTile

* Update TileUpdateRequester usages to account for multiple Tiles

* Add entity count to Shortcut Tile list in Settings

* Fix ktlint errors

* Fix more ktlint errors

* Extract string resource

* Add MULTI_INSTANCES_SUPPORTED to ShortcutsTile to be able to use any number of Tiles

* Refresh the list of Shortcut Tiles in the Settings without needing to restart the app

* Remove test logs

* Update androidx.wear.tiles:tiles to the latest version which doesn't yet require API 34 compileSdk version

* Fix crash when the preference's value is "{}"

* Fix crash when key String is "null" and converting to Int

* Rename placeholder variable name

* Add a comment explaining why to save the tiles in a getter

* Return emptyList() directly for clarity

* Remove icons from "Shortcut tile #n" entries in Settings

* Pass emptyList instead of using the non-null assertion operator to prevent NPEs in edge cases

* Refactor getTileShortcuts and getAllTileShortcuts in WearPrefsRepositoryImpl.
Make the code more readable and understandable, and reduce code duplication.

* Make it explicit that intKey is only intended to be null if stringKey == "null"

* Rename getTileShortcuts to getTileShortcutsAndSaveTileId and make it save the tileId not only when there's a null key in the map, but also when the tileId is not yet in the map. This way, actual Tiles and the tileId keys will be more in sync.

* Handle adding Shortcuts Tile immediately after updating the app to a new version which introduces support for multiple Shortcuts Tiles.

* Show message in the Settings when there are no Shortcuts tiles added yet.

* Refine message in the Settings when there are no Shortcuts tiles added yet

* WIP: ConfigShortcutsTile action

* Update comments about Wear OS versions

* Finalize ConfigShortcutsTile feature by applying @jpelgrom's suggestion to OpenShortcutTileSettingsActivity

* Only wrap the code in runCatching which is expected to throw an Exception under normal circumstances, when the pref value needs to be migrated from the old format.

* Call getTileShortcutsAndSaveTileId in OpenShortcutTileSettingsActivity

* Remove unnecessary stuff
This commit is contained in:
Márton Maráz 2023-07-31 21:14:01 +02:00 committed by GitHub
parent fcab330115
commit a8a7363317
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 421 additions and 100 deletions

View File

@ -1,8 +1,10 @@
package io.homeassistant.companion.android.common.data.prefs
interface WearPrefsRepository {
suspend fun getTileShortcuts(): List<String>
suspend fun setTileShortcuts(entities: List<String>)
suspend fun getAllTileShortcuts(): Map<Int?, List<String>>
suspend fun getTileShortcutsAndSaveTileId(tileId: Int): List<String>
suspend fun setTileShortcuts(tileId: Int?, entities: List<String>)
suspend fun removeTileShortcuts(tileId: Int?): List<String>?
suspend fun getShowShortcutText(): Boolean
suspend fun setShowShortcutTextEnabled(enabled: Boolean)
suspend fun getTemplateTileContent(): String

View File

@ -1,8 +1,10 @@
package io.homeassistant.companion.android.common.data.prefs
import io.homeassistant.companion.android.common.data.LocalStorage
import io.homeassistant.companion.android.common.util.toStringList
import kotlinx.coroutines.runBlocking
import org.json.JSONArray
import org.json.JSONObject
import javax.inject.Inject
import javax.inject.Named
@ -52,15 +54,68 @@ class WearPrefsRepositoryImpl @Inject constructor(
}
}
override suspend fun getTileShortcuts(): List<String> {
val jsonArray = JSONArray(localStorage.getString(PREF_TILE_SHORTCUTS) ?: "[]")
return List(jsonArray.length()) {
jsonArray.getString(it)
override suspend fun getTileShortcutsAndSaveTileId(tileId: Int): List<String> {
val tileIdToShortcutsMap = getAllTileShortcuts()
return if (null in tileIdToShortcutsMap && tileId !in tileIdToShortcutsMap) {
// if there are shortcuts with an unknown (null) tileId key from a previous installation,
// and the tileId parameter is not already present in the map, associate it with those shortcuts
val entities = removeTileShortcuts(null)!!
setTileShortcuts(tileId, entities)
entities
} else {
val entities = tileIdToShortcutsMap[tileId]
if (entities == null) {
setTileShortcuts(tileId, emptyList())
}
entities ?: emptyList()
}
}
override suspend fun setTileShortcuts(entities: List<String>) {
localStorage.putString(PREF_TILE_SHORTCUTS, JSONArray(entities).toString())
override suspend fun getAllTileShortcuts(): Map<Int?, List<String>> {
return localStorage.getString(PREF_TILE_SHORTCUTS)?.let { jsonStr ->
runCatching {
JSONObject(jsonStr)
}.fold(
onSuccess = { jsonObject ->
buildMap {
jsonObject.keys().forEach { stringKey ->
val intKey = stringKey.takeUnless { it == "null" }?.toInt()
val jsonArray = jsonObject.getJSONArray(stringKey)
val entities = jsonArray.toStringList()
put(intKey, entities)
}
}
},
onFailure = {
// backward compatibility with the previous format when there was only one Shortcut Tile:
val jsonArray = JSONArray(jsonStr)
val entities = jsonArray.toStringList()
mapOf(
null to entities // the key is null since we don't (yet) have the tileId
)
}
)
} ?: emptyMap()
}
override suspend fun setTileShortcuts(tileId: Int?, entities: List<String>) {
val map = getAllTileShortcuts() + mapOf(tileId to entities)
setTileShortcuts(map)
}
private suspend fun setTileShortcuts(map: Map<Int?, List<String>>) {
val jsonArrayMap = map.map { (tileId, entities) ->
tileId.toString() to JSONArray(entities)
}.toMap()
val jsonStr = JSONObject(jsonArrayMap).toString()
localStorage.putString(PREF_TILE_SHORTCUTS, jsonStr)
}
override suspend fun removeTileShortcuts(tileId: Int?): List<String>? {
val tileShortcutsMap = getAllTileShortcuts().toMutableMap()
val entities = tileShortcutsMap.remove(tileId)
setTileShortcuts(tileShortcutsMap)
return entities
}
override suspend fun getTemplateTileContent(): String {

View File

@ -0,0 +1,8 @@
package io.homeassistant.companion.android.common.util
import org.json.JSONArray
fun JSONArray.toStringList(): List<String> =
List(length()) { i ->
getString(i)
}

View File

@ -217,6 +217,10 @@
<item quantity="one">%d entity found</item>
<item quantity="other">%d entities found</item>
</plurals>
<plurals name="n_entities">
<item quantity="one">1 entity</item>
<item quantity="other">%d entities</item>
</plurals>
<string name="entity_attribute_checkbox">Append Attribute Value</string>
<string name="entity_id">Entity ID</string>
<string name="entity_id_name">Entity ID: %1$s</string>
@ -732,11 +736,14 @@
<string name="shortcuts_choose">Choose shortcuts</string>
<string name="shortcut5_note">Not all launchers support displaying 5 shortcuts, you may not see this shortcut if the above 4 are already added.</string>
<string name="shortcuts_summary">Add shortcuts to launcher</string>
<string name="shortcuts_tile">Shortcuts tile</string>
<string name="shortcut_tiles">Shortcut tiles</string>
<string name="shortcuts_tile_n">Shortcuts tile #%d</string>
<string name="shortcuts_tile_select">Select shortcuts tile to manage</string>
<string name="shortcuts_tile_description">Select up to 7 entities</string>
<string name="shortcuts_tile_empty">Choose entities in settings</string>
<string name="shortcuts_tile_text_setting">Show names on shortcuts tile</string>
<string name="shortcuts_tile_log_in">Log in to Home Assistant to add your first shortcut</string>
<string name="shortcuts_tile_no_tiles_yet">There are no Shortcut tiles added yet</string>
<string name="shortcuts">Shortcuts</string>
<string name="show">Show</string>
<string name="show_share_logs_summary">Sharing logs with the Home Assistant team will help to solve issues. Please share the logs only if you have been asked to do so by a Home Assistant developer</string>

View File

@ -50,7 +50,7 @@ sentry-android = "6.26.0"
watchfaceComplicationsDataSourceKtx = "1.1.1"
wear = "1.2.0"
wear-compose-foundation = "1.1.2"
wear-tiles = "1.1.0"
wear-tiles = "1.2.0-alpha07"
wearPhoneInteractions = "1.0.1"
wearInput = "1.2.0-alpha02"
webkit = "1.7.0"

View File

@ -83,6 +83,17 @@
</intent-filter>
</activity>
<activity
android:name=".tiles.OpenShortcutTileSettingsActivity"
android:exported="true"
android:excludeFromRecents="true">
<intent-filter>
<action android:name="ConfigShortcutsTile" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="com.google.android.clockwork.tiles.category.PROVIDER_CONFIG" />
</intent-filter>
</activity>
<!-- To show confirmations and failures -->
<activity android:name="androidx.wear.activity.ConfirmationActivity" />
@ -122,6 +133,14 @@
<meta-data android:name="androidx.wear.tiles.PREVIEW"
android:resource="@drawable/favorite_entities_tile_example" />
<meta-data
android:name="com.google.android.clockwork.tiles.MULTI_INSTANCES_SUPPORTED"
android:value="true" /> <!-- This is supported starting from Wear OS 3 -->
<meta-data
android:name="com.google.android.clockwork.tiles.PROVIDER_CONFIG_ACTION"
android:value="ConfigShortcutsTile" />
</service>
<service
android:name=".tiles.TemplateTile"
@ -219,4 +238,4 @@
</service>
</application>
</manifest>
</manifest>

View File

@ -6,10 +6,12 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.home.views.DEEPLINK_PREFIX_SET_SHORTCUT_TILE
import io.homeassistant.companion.android.home.views.LoadHomePage
import io.homeassistant.companion.android.onboarding.OnboardingActivity
import io.homeassistant.companion.android.sensors.SensorReceiver
@ -34,6 +36,16 @@ class HomeActivity : ComponentActivity(), HomeView {
fun newInstance(context: Context): Intent {
return Intent(context, HomeActivity::class.java)
}
fun getShortcutsTileSettingsIntent(
context: Context,
tileId: Int
) = Intent(
Intent.ACTION_VIEW,
"$DEEPLINK_PREFIX_SET_SHORTCUT_TILE/$tileId".toUri(),
context,
HomeActivity::class.java
)
}
override fun onCreate(savedInstanceState: Bundle?) {

View File

@ -38,8 +38,9 @@ interface HomePresenter {
suspend fun getDeviceRegistryUpdates(): Flow<DeviceRegistryUpdatedEvent>?
suspend fun getEntityRegistryUpdates(): Flow<EntityRegistryUpdatedEvent>?
suspend fun getTileShortcuts(): List<SimplifiedEntity>
suspend fun setTileShortcuts(entities: List<SimplifiedEntity>)
suspend fun getAllTileShortcuts(): Map<Int?, List<SimplifiedEntity>>
suspend fun getTileShortcuts(tileId: Int): List<SimplifiedEntity>
suspend fun setTileShortcuts(tileId: Int?, entities: List<SimplifiedEntity>)
suspend fun getWearHapticFeedback(): Boolean
suspend fun setWearHapticFeedback(enabled: Boolean)

View File

@ -233,12 +233,20 @@ class HomePresenterImpl @Inject constructor(
return serverManager.webSocketRepository().getEntityRegistryUpdates()
}
override suspend fun getTileShortcuts(): List<SimplifiedEntity> {
return wearPrefsRepository.getTileShortcuts().map { SimplifiedEntity(it) }
override suspend fun getAllTileShortcuts(): Map<Int?, List<SimplifiedEntity>> {
return wearPrefsRepository.getAllTileShortcuts().mapValues { (_, entities) ->
entities.map {
SimplifiedEntity(it)
}
}
}
override suspend fun setTileShortcuts(entities: List<SimplifiedEntity>) {
wearPrefsRepository.setTileShortcuts(entities.map { it.entityString })
override suspend fun getTileShortcuts(tileId: Int): List<SimplifiedEntity> {
return wearPrefsRepository.getTileShortcutsAndSaveTileId(tileId).map { SimplifiedEntity(it) }
}
override suspend fun setTileShortcuts(tileId: Int?, entities: List<SimplifiedEntity>) {
wearPrefsRepository.setTileShortcuts(tileId, entities.map { it.entityString })
}
override suspend fun getWearHapticFeedback(): Boolean {

View File

@ -11,6 +11,7 @@ import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
@ -85,8 +86,8 @@ class MainViewModel @Inject constructor(
val favoriteEntityIds = favoritesDao.getAllFlow().collectAsState()
private val favoriteCaches = favoriteCachesDao.getAll()
var shortcutEntities = mutableStateListOf<SimplifiedEntity>()
private set
val shortcutEntitiesMap = mutableStateMapOf<Int?, SnapshotStateList<SimplifiedEntity>>()
var areas = mutableListOf<AreaRegistryResponse>()
private set
@ -136,7 +137,7 @@ class MainViewModel @Inject constructor(
if (!homePresenter.isConnected()) {
return@launch
}
shortcutEntities.addAll(homePresenter.getTileShortcuts())
loadShortcutTileEntities()
isHapticEnabled.value = homePresenter.getWearHapticFeedback()
isToastEnabled.value = homePresenter.getWearToastConfirmation()
isShowShortcutTextEnabled.value = homePresenter.getShowShortcutText()
@ -153,6 +154,16 @@ class MainViewModel @Inject constructor(
}
}
fun loadShortcutTileEntities() {
viewModelScope.launch {
val map = homePresenter.getAllTileShortcuts().mapValues { (_, entities) ->
entities.toMutableStateList()
}
shortcutEntitiesMap.clear()
shortcutEntitiesMap.putAll(map)
}
}
fun loadEntities() {
if (!homePresenter.isConnected()) return
viewModelScope.launch {
@ -401,22 +412,24 @@ class MainViewModel @Inject constructor(
}
}
fun setTileShortcut(index: Int, entity: SimplifiedEntity) {
fun setTileShortcut(tileId: Int?, index: Int, entity: SimplifiedEntity) {
viewModelScope.launch {
val shortcutEntities = shortcutEntitiesMap[tileId]!!
if (index < shortcutEntities.size) {
shortcutEntities[index] = entity
} else {
shortcutEntities.add(entity)
}
homePresenter.setTileShortcuts(shortcutEntities)
homePresenter.setTileShortcuts(tileId, entities = shortcutEntities)
}
}
fun clearTileShortcut(index: Int) {
fun clearTileShortcut(tileId: Int?, index: Int) {
viewModelScope.launch {
val shortcutEntities = shortcutEntitiesMap[tileId]!!
if (index < shortcutEntities.size) {
shortcutEntities.removeAt(index)
homePresenter.setTileShortcuts(shortcutEntities)
homePresenter.setTileShortcuts(tileId, entities = shortcutEntities)
}
}
}

View File

@ -1,10 +1,6 @@
package io.homeassistant.companion.android.home.views
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavType
import androidx.navigation.navArgument
@ -21,6 +17,8 @@ import io.homeassistant.companion.android.tiles.TemplateTile
import io.homeassistant.companion.android.views.ChooseEntityView
private const val ARG_SCREEN_SENSOR_MANAGER_ID = "sensorManagerId"
private const val ARG_SCREEN_SHORTCUTS_TILE_ID = "shortcutsTileId"
private const val ARG_SCREEN_SHORTCUTS_TILE_ENTITY_INDEX = "shortcutsTileEntityIndex"
private const val SCREEN_LANDING = "landing"
private const val SCREEN_ENTITY_DETAIL = "entity_detail"
@ -29,18 +27,20 @@ private const val SCREEN_MANAGE_SENSORS = "manage_all_sensors"
private const val SCREEN_SINGLE_SENSOR_MANAGER = "sensor_manager"
private const val SCREEN_SETTINGS = "settings"
private const val SCREEN_SET_FAVORITES = "set_favorites"
private const val SCREEN_SET_TILE_SHORTCUTS = "set_tile_shortcuts"
private const val SCREEN_SELECT_TILE_SHORTCUT = "select_tile_shortcut"
private const val ROUTE_SHORTCUTS_TILE = "shortcuts_tile"
private const val SCREEN_SELECT_SHORTCUTS_TILE = "select_shortcuts_tile"
private const val SCREEN_SET_SHORTCUTS_TILE = "set_shortcuts_tile"
private const val SCREEN_SHORTCUTS_TILE_CHOOSE_ENTITY = "shortcuts_tile_choose_entity"
private const val SCREEN_SET_TILE_TEMPLATE = "set_tile_template"
private const val SCREEN_SET_TILE_TEMPLATE_REFRESH_INTERVAL = "set_tile_template_refresh_interval"
const val DEEPLINK_SENSOR_MANAGER = "ha_wear://$SCREEN_SINGLE_SENSOR_MANAGER"
const val DEEPLINK_PREFIX_SET_SHORTCUT_TILE = "ha_wear://$SCREEN_SET_SHORTCUTS_TILE"
@Composable
fun LoadHomePage(
mainViewModel: MainViewModel
) {
var shortcutEntitySelectionIndex: Int by remember { mutableStateOf(0) }
val context = LocalContext.current
WearAppTheme {
@ -129,8 +129,9 @@ fun LoadHomePage(
},
onClearFavorites = { mainViewModel.clearFavorites() },
onClickSetShortcuts = {
mainViewModel.loadShortcutTileEntities()
swipeDismissableNavController.navigate(
SCREEN_SET_TILE_SHORTCUTS
"$ROUTE_SHORTCUTS_TILE/$SCREEN_SELECT_SHORTCUTS_TILE"
)
},
onClickSensors = {
@ -162,33 +163,63 @@ fun LoadHomePage(
}
}
}
composable(SCREEN_SET_TILE_SHORTCUTS) {
SetTileShortcutsView(
shortcutEntities = mainViewModel.shortcutEntities,
onShortcutEntitySelectionChange = {
shortcutEntitySelectionIndex = it
swipeDismissableNavController.navigate(SCREEN_SELECT_TILE_SHORTCUT)
composable("$ROUTE_SHORTCUTS_TILE/$SCREEN_SELECT_SHORTCUTS_TILE") {
SelectShortcutsTileView(
shortcutTileEntitiesCountById = mainViewModel.shortcutEntitiesMap.mapValues { (_, entities) -> entities.size },
onSelectShortcutsTile = { tileId ->
swipeDismissableNavController.navigate("$ROUTE_SHORTCUTS_TILE/$tileId/$SCREEN_SET_SHORTCUTS_TILE")
},
isShowShortcutTextEnabled = mainViewModel.isShowShortcutTextEnabled.value,
onShowShortcutTextEnabled = {
mainViewModel.setShowShortcutTextEnabled(it)
TileService.getUpdater(context).requestUpdate(ShortcutsTile::class.java)
ShortcutsTile.requestUpdate(context)
}
)
}
composable(SCREEN_SELECT_TILE_SHORTCUT) {
composable(
route = "$ROUTE_SHORTCUTS_TILE/{$ARG_SCREEN_SHORTCUTS_TILE_ID}/$SCREEN_SET_SHORTCUTS_TILE",
arguments = listOf(
navArgument(name = ARG_SCREEN_SHORTCUTS_TILE_ID) {
type = NavType.StringType
}
),
deepLinks = listOf(
navDeepLink { uriPattern = "$DEEPLINK_PREFIX_SET_SHORTCUT_TILE/{$ARG_SCREEN_SHORTCUTS_TILE_ID}" }
)
) { backStackEntry ->
val tileId = backStackEntry.arguments!!.getString(ARG_SCREEN_SHORTCUTS_TILE_ID)!!.toIntOrNull()
SetShortcutsTileView(
shortcutEntities = mainViewModel.shortcutEntitiesMap[tileId] ?: emptyList(),
onShortcutEntitySelectionChange = { entityIndex ->
swipeDismissableNavController.navigate("$ROUTE_SHORTCUTS_TILE/$tileId/$SCREEN_SHORTCUTS_TILE_CHOOSE_ENTITY/$entityIndex")
}
)
}
composable(
route = "$ROUTE_SHORTCUTS_TILE/{$ARG_SCREEN_SHORTCUTS_TILE_ID}/$SCREEN_SHORTCUTS_TILE_CHOOSE_ENTITY/{$ARG_SCREEN_SHORTCUTS_TILE_ENTITY_INDEX}",
arguments = listOf(
navArgument(name = ARG_SCREEN_SHORTCUTS_TILE_ID) {
type = NavType.StringType
},
navArgument(name = ARG_SCREEN_SHORTCUTS_TILE_ENTITY_INDEX) {
type = NavType.IntType
}
)
) { backStackEntry ->
val entityIndex = backStackEntry.arguments!!.getInt(ARG_SCREEN_SHORTCUTS_TILE_ENTITY_INDEX)
val tileId = backStackEntry.arguments!!.getString(ARG_SCREEN_SHORTCUTS_TILE_ID)!!.toIntOrNull()
ChooseEntityView(
entitiesByDomainOrder = mainViewModel.entitiesByDomainOrder,
entitiesByDomain = mainViewModel.entitiesByDomain,
favoriteEntityIds = mainViewModel.favoriteEntityIds,
onNoneClicked = {
mainViewModel.clearTileShortcut(shortcutEntitySelectionIndex)
TileService.getUpdater(context).requestUpdate(ShortcutsTile::class.java)
mainViewModel.clearTileShortcut(tileId, entityIndex)
ShortcutsTile.requestUpdate(context)
swipeDismissableNavController.navigateUp()
},
onEntitySelected = { entity ->
mainViewModel.setTileShortcut(shortcutEntitySelectionIndex, entity)
TileService.getUpdater(context).requestUpdate(ShortcutsTile::class.java)
mainViewModel.setTileShortcut(tileId, entityIndex, entity)
ShortcutsTile.requestUpdate(context)
swipeDismissableNavController.navigateUp()
}
)

View File

@ -0,0 +1,136 @@
package io.homeassistant.companion.android.home.views
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.PositionIndicator
import androidx.wear.compose.material.Scaffold
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.ToggleChip
import androidx.wear.compose.material.ToggleChipDefaults
import androidx.wear.compose.material.itemsIndexed
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.theme.WearAppTheme
import io.homeassistant.companion.android.theme.wearColorPalette
import io.homeassistant.companion.android.views.ListHeader
import io.homeassistant.companion.android.views.ThemeLazyColumn
import io.homeassistant.companion.android.common.R as commonR
@Composable
fun SelectShortcutsTileView(
shortcutTileEntitiesCountById: Map<Int?, Int>,
onSelectShortcutsTile: (tileId: Int?) -> Unit,
isShowShortcutTextEnabled: Boolean,
onShowShortcutTextEnabled: (Boolean) -> Unit
) {
val scalingLazyListState = rememberScalingLazyListState()
WearAppTheme {
Scaffold(
positionIndicator = {
if (scalingLazyListState.isScrollInProgress) {
PositionIndicator(scalingLazyListState = scalingLazyListState)
}
},
timeText = { TimeText(scalingLazyListState = scalingLazyListState) }
) {
ThemeLazyColumn(state = scalingLazyListState) {
item {
ListHeader(id = commonR.string.shortcut_tiles)
}
item {
ToggleChip(
modifier = Modifier.fillMaxWidth(),
checked = isShowShortcutTextEnabled,
onCheckedChange = { onShowShortcutTextEnabled(it) },
label = {
Text(stringResource(commonR.string.shortcuts_tile_text_setting))
},
appIcon = {
Image(
asset =
if (isShowShortcutTextEnabled) {
CommunityMaterial.Icon.cmd_alphabetical
} else {
CommunityMaterial.Icon.cmd_alphabetical_off
},
colorFilter = ColorFilter.tint(wearColorPalette.onSurface)
)
},
toggleControl = {
Icon(
imageVector = ToggleChipDefaults.checkboxIcon(isShowShortcutTextEnabled),
contentDescription = if (isShowShortcutTextEnabled) {
stringResource(commonR.string.show)
} else {
stringResource(commonR.string.hide)
}
)
}
)
}
item {
ListHeader(id = commonR.string.shortcuts_tile_select)
}
if (shortcutTileEntitiesCountById.isEmpty()) {
item {
Text(stringResource(commonR.string.shortcuts_tile_no_tiles_yet))
}
} else {
itemsIndexed(shortcutTileEntitiesCountById.keys.toList()) { index, shortcutsTileId ->
Chip(
modifier = Modifier
.fillMaxWidth(),
label = {
Text(stringResource(commonR.string.shortcuts_tile_n, index + 1))
},
secondaryLabel = {
val entityCount = shortcutTileEntitiesCountById[shortcutsTileId] ?: 0
if (entityCount > 0) {
Text(pluralStringResource(commonR.plurals.n_entities, entityCount, entityCount))
}
},
onClick = { onSelectShortcutsTile(shortcutsTileId) },
colors = ChipDefaults.secondaryChipColors()
)
}
}
}
}
}
}
@Preview(device = Devices.WEAR_OS_LARGE_ROUND)
@Composable
private fun PreviewSelectShortcutsTileView() {
SelectShortcutsTileView(
shortcutTileEntitiesCountById = mapOf(
null to 7,
1111 to 1,
2222 to 0
),
onSelectShortcutsTile = {},
isShowShortcutTextEnabled = true,
onShowShortcutTextEnabled = {}
)
}
@Preview(device = Devices.WEAR_OS_LARGE_ROUND)
@Composable
private fun PreviewSelectShortcutsTileEmptyView() {
SelectShortcutsTileView(
shortcutTileEntitiesCountById = emptyMap(),
onSelectShortcutsTile = {},
isShowShortcutTextEnabled = true,
onShowShortcutTextEnabled = {}
)
}

View File

@ -16,18 +16,14 @@ import androidx.wear.compose.material.Button
import androidx.wear.compose.material.ButtonDefaults
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.PositionIndicator
import androidx.wear.compose.material.Scaffold
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.data.SimplifiedEntity
import io.homeassistant.companion.android.theme.WearAppTheme
import io.homeassistant.companion.android.theme.wearColorPalette
import io.homeassistant.companion.android.util.getIcon
import io.homeassistant.companion.android.util.simplifiedEntity
import io.homeassistant.companion.android.views.ListHeader
@ -35,11 +31,9 @@ import io.homeassistant.companion.android.views.ThemeLazyColumn
import io.homeassistant.companion.android.common.R as commonR
@Composable
fun SetTileShortcutsView(
shortcutEntities: MutableList<SimplifiedEntity>,
onShortcutEntitySelectionChange: (Int) -> Unit,
isShowShortcutTextEnabled: Boolean,
onShowShortcutTextEnabled: (Boolean) -> Unit
fun SetShortcutsTileView(
shortcutEntities: List<SimplifiedEntity>,
onShortcutEntitySelectionChange: (Int) -> Unit
) {
val scalingLazyListState = rememberScalingLazyListState()
WearAppTheme {
@ -52,40 +46,6 @@ fun SetTileShortcutsView(
timeText = { TimeText(scalingLazyListState = scalingLazyListState) }
) {
ThemeLazyColumn(state = scalingLazyListState) {
item {
ListHeader(id = commonR.string.shortcuts_tile)
}
item {
ToggleChip(
modifier = Modifier.fillMaxWidth(),
checked = isShowShortcutTextEnabled,
onCheckedChange = { onShowShortcutTextEnabled(it) },
label = {
Text(stringResource(commonR.string.shortcuts_tile_text_setting))
},
appIcon = {
Image(
asset =
if (isShowShortcutTextEnabled) {
CommunityMaterial.Icon.cmd_alphabetical
} else {
CommunityMaterial.Icon.cmd_alphabetical_off
},
colorFilter = ColorFilter.tint(wearColorPalette.onSurface)
)
},
toggleControl = {
Icon(
imageVector = ToggleChipDefaults.checkboxIcon(isShowShortcutTextEnabled),
contentDescription = if (isShowShortcutTextEnabled) {
stringResource(commonR.string.show)
} else {
stringResource(commonR.string.hide)
}
)
}
)
}
item {
ListHeader(id = commonR.string.shortcuts_choose)
}
@ -143,10 +103,8 @@ fun SetTileShortcutsView(
@Preview(device = Devices.WEAR_OS_LARGE_ROUND)
@Composable
private fun PreviewSetTileShortcutsView() {
SetTileShortcutsView(
SetShortcutsTileView(
shortcutEntities = mutableListOf(simplifiedEntity),
onShortcutEntitySelectionChange = {},
isShowShortcutTextEnabled = true,
onShowShortcutTextEnabled = {}
onShortcutEntitySelectionChange = {}
)
}

View File

@ -216,7 +216,7 @@ fun SettingsView(
item {
SecondarySettingsChip(
icon = CommunityMaterial.Icon3.cmd_star_circle_outline,
label = stringResource(commonR.string.shortcuts_tile),
label = stringResource(commonR.string.shortcut_tiles),
onClick = onClickSetShortcuts
)
}

View File

@ -58,7 +58,8 @@ class MobileAppIntegrationPresenterImpl @Inject constructor(
private fun updateTiles() = mainScope.launch {
try {
val updater = TileService.getUpdater(view as Context)
val context = view as Context
val updater = TileService.getUpdater(context)
updater.requestUpdate(ConversationTile::class.java)
updater.requestUpdate(ShortcutsTile::class.java)
updater.requestUpdate(TemplateTile::class.java)

View File

@ -0,0 +1,33 @@
package io.homeassistant.companion.android.tiles
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.common.data.prefs.WearPrefsRepositoryImpl
import io.homeassistant.companion.android.home.HomeActivity
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class OpenShortcutTileSettingsActivity : AppCompatActivity() {
@Inject
lateinit var wearPrefsRepository: WearPrefsRepositoryImpl
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val tileId = intent.extras?.getInt("com.google.android.clockwork.EXTRA_PROVIDER_CONFIG_TILE_ID")
tileId?.takeIf { it != 0 }?.let {
lifecycleScope.launch {
wearPrefsRepository.getTileShortcutsAndSaveTileId(tileId)
}
val intent = HomeActivity.getShortcutsTileSettingsIntent(
context = this,
tileId = it
)
startActivity(intent)
}
finish()
}
}

View File

@ -1,5 +1,6 @@
package io.homeassistant.companion.android.tiles
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
@ -9,6 +10,7 @@ import androidx.wear.tiles.ActionBuilders
import androidx.wear.tiles.ColorBuilders.argb
import androidx.wear.tiles.DimensionBuilders.dp
import androidx.wear.tiles.DimensionBuilders.sp
import androidx.wear.tiles.EventBuilders
import androidx.wear.tiles.LayoutElementBuilders
import androidx.wear.tiles.LayoutElementBuilders.Box
import androidx.wear.tiles.LayoutElementBuilders.Column
@ -41,6 +43,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.guava.future
import kotlinx.coroutines.launch
import java.nio.ByteBuffer
import javax.inject.Inject
import kotlin.math.min
@ -78,13 +81,14 @@ class ShortcutsTile : TileService() {
}
}
val entities = getEntities()
val tileId = requestParams.tileId
val entities = getEntities(tileId)
Tile.Builder()
.setResourcesVersion(entities.toString())
.setTimeline(
if (serverManager.isRegistered()) {
timeline()
timeline(tileId)
} else {
loggedOutTimeline(
this@ShortcutsTile,
@ -102,7 +106,7 @@ class ShortcutsTile : TileService() {
val iconSize = if (showLabels) ICON_SIZE_SMALL else ICON_SIZE_FULL
val density = requestParams.deviceParameters!!.screenDensity
val iconSizePx = (iconSize * density).roundToInt()
val entities = getEntities()
val entities = getEntities(requestParams.tileId)
Resources.Builder()
.setVersion(entities.toString())
@ -143,18 +147,45 @@ class ShortcutsTile : TileService() {
.build()
}
override fun onTileAddEvent(requestParams: EventBuilders.TileAddEvent) {
serviceScope.launch {
/**
* When the app is updated from an older version (which only supported a single Shortcut Tile),
* and the user is adding a new Shortcuts Tile, we can't tell for sure if it's the 1st or 2nd Tile.
* Even though we may have the shortcut list stored in the prefs, it doesn't guarantee that
* the tile was actually added to the Tiles carousel.
* The [WearPrefsRepositoryImpl::getTileShortcutsAndSaveTileId] method will handle both of the following cases:
* 1. There was no Tile added, but there were shortcuts stored in the prefs.
* In this case, the stored shortcuts will be associated to the new tileId.
* 2. There was a single Tile added, and there were shortcuts stored in the prefs.
* If there was a Tile update since updating the app, the tileId will be already
* associated to the shortcuts, because it also calls [getTileShortcutsAndSaveTileId].
* If there was no Tile update yet, the new Tile will "steal" the shortcuts from the existing Tile,
* and the old Tile will behave as it is the new Tile. This is needed because
* we don't know if it's the 1st or 2nd Tile.
*/
wearPrefsRepository.getTileShortcutsAndSaveTileId(requestParams.tileId)
}
}
override fun onTileRemoveEvent(requestParams: EventBuilders.TileRemoveEvent) {
serviceScope.launch {
wearPrefsRepository.removeTileShortcuts(requestParams.tileId)
}
}
override fun onDestroy() {
super.onDestroy()
// Cleans up the coroutine
serviceJob.cancel()
}
private suspend fun getEntities(): List<SimplifiedEntity> {
return wearPrefsRepository.getTileShortcuts().map { SimplifiedEntity(it) }
private suspend fun getEntities(tileId: Int): List<SimplifiedEntity> {
return wearPrefsRepository.getTileShortcutsAndSaveTileId(tileId).map { SimplifiedEntity(it) }
}
private suspend fun timeline(): Timeline {
val entities = getEntities()
private suspend fun timeline(tileId: Int): Timeline {
val entities = getEntities(tileId)
val showLabels = wearPrefsRepository.getShowShortcutText()
return Timeline.fromLayoutElement(layout(entities, showLabels))
@ -253,4 +284,10 @@ class ShortcutsTile : TileService() {
}
}
.build()
companion object {
fun requestUpdate(context: Context) {
getUpdater(context).requestUpdate(ShortcutsTile::class.java)
}
}
}