From a8a7363317823671bb523570b99faae839b7e470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Mar=C3=A1z?= Date: Mon, 31 Jul 2023 21:14:01 +0200 Subject: [PATCH] Implement support for multiple Shortcut Tiles on Wear OS (#3697) * Extract JSONArray -> List 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 --- .../common/data/prefs/WearPrefsRepository.kt | 6 +- .../data/prefs/WearPrefsRepositoryImpl.kt | 67 ++++++++- .../companion/android/common/util/JsonUtil.kt | 8 ++ common/src/main/res/values/strings.xml | 9 +- gradle/libs.versions.toml | 2 +- wear/src/main/AndroidManifest.xml | 21 ++- .../companion/android/home/HomeActivity.kt | 12 ++ .../companion/android/home/HomePresenter.kt | 5 +- .../android/home/HomePresenterImpl.kt | 16 ++- .../companion/android/home/MainViewModel.kt | 27 +++- .../companion/android/home/views/HomeView.kt | 71 ++++++--- .../home/views/SelectShortcutsTileView.kt | 136 ++++++++++++++++++ ...ortcutsView.kt => SetShortcutsTileView.kt} | 52 +------ .../android/home/views/SettingsView.kt | 2 +- .../MobileAppIntegrationPresenterImpl.kt | 3 +- .../tiles/OpenShortcutTileSettingsActivity.kt | 33 +++++ .../companion/android/tiles/ShortcutsTile.kt | 51 ++++++- 17 files changed, 421 insertions(+), 100 deletions(-) create mode 100644 common/src/main/java/io/homeassistant/companion/android/common/util/JsonUtil.kt create mode 100644 wear/src/main/java/io/homeassistant/companion/android/home/views/SelectShortcutsTileView.kt rename wear/src/main/java/io/homeassistant/companion/android/home/views/{SetTileShortcutsView.kt => SetShortcutsTileView.kt} (66%) create mode 100644 wear/src/main/java/io/homeassistant/companion/android/tiles/OpenShortcutTileSettingsActivity.kt diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepository.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepository.kt index 89b07c87a..b2beb6b84 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepository.kt +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepository.kt @@ -1,8 +1,10 @@ package io.homeassistant.companion.android.common.data.prefs interface WearPrefsRepository { - suspend fun getTileShortcuts(): List - suspend fun setTileShortcuts(entities: List) + suspend fun getAllTileShortcuts(): Map> + suspend fun getTileShortcutsAndSaveTileId(tileId: Int): List + suspend fun setTileShortcuts(tileId: Int?, entities: List) + suspend fun removeTileShortcuts(tileId: Int?): List? suspend fun getShowShortcutText(): Boolean suspend fun setShowShortcutTextEnabled(enabled: Boolean) suspend fun getTemplateTileContent(): String diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepositoryImpl.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepositoryImpl.kt index fc8d8398f..7f0bf4595 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepositoryImpl.kt +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepositoryImpl.kt @@ -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 { - val jsonArray = JSONArray(localStorage.getString(PREF_TILE_SHORTCUTS) ?: "[]") - return List(jsonArray.length()) { - jsonArray.getString(it) + override suspend fun getTileShortcutsAndSaveTileId(tileId: Int): List { + 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) { - localStorage.putString(PREF_TILE_SHORTCUTS, JSONArray(entities).toString()) + override suspend fun getAllTileShortcuts(): Map> { + 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) { + val map = getAllTileShortcuts() + mapOf(tileId to entities) + setTileShortcuts(map) + } + + private suspend fun setTileShortcuts(map: Map>) { + 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? { + val tileShortcutsMap = getAllTileShortcuts().toMutableMap() + val entities = tileShortcutsMap.remove(tileId) + setTileShortcuts(tileShortcutsMap) + return entities } override suspend fun getTemplateTileContent(): String { diff --git a/common/src/main/java/io/homeassistant/companion/android/common/util/JsonUtil.kt b/common/src/main/java/io/homeassistant/companion/android/common/util/JsonUtil.kt new file mode 100644 index 000000000..23ea7b4d1 --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/common/util/JsonUtil.kt @@ -0,0 +1,8 @@ +package io.homeassistant.companion.android.common.util + +import org.json.JSONArray + +fun JSONArray.toStringList(): List = + List(length()) { i -> + getString(i) + } diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index ff938b5d8..9feb477de 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -217,6 +217,10 @@ %d entity found %d entities found + + 1 entity + %d entities + Append Attribute Value Entity ID Entity ID: %1$s @@ -732,11 +736,14 @@ Choose shortcuts Not all launchers support displaying 5 shortcuts, you may not see this shortcut if the above 4 are already added. Add shortcuts to launcher - Shortcuts tile + Shortcut tiles + Shortcuts tile #%d + Select shortcuts tile to manage Select up to 7 entities Choose entities in settings Show names on shortcuts tile Log in to Home Assistant to add your first shortcut + There are no Shortcut tiles added yet Shortcuts Show 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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 33ded3396..e43c91b6b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index e16e8b678..aa63f4c47 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -83,6 +83,17 @@ + + + + + + + + @@ -122,6 +133,14 @@ + + + + - \ No newline at end of file + diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/HomeActivity.kt b/wear/src/main/java/io/homeassistant/companion/android/home/HomeActivity.kt index 938c14fe0..4e6183707 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/HomeActivity.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/HomeActivity.kt @@ -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?) { diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenter.kt b/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenter.kt index d0d5bf3d0..a99de612e 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenter.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenter.kt @@ -38,8 +38,9 @@ interface HomePresenter { suspend fun getDeviceRegistryUpdates(): Flow? suspend fun getEntityRegistryUpdates(): Flow? - suspend fun getTileShortcuts(): List - suspend fun setTileShortcuts(entities: List) + suspend fun getAllTileShortcuts(): Map> + suspend fun getTileShortcuts(tileId: Int): List + suspend fun setTileShortcuts(tileId: Int?, entities: List) suspend fun getWearHapticFeedback(): Boolean suspend fun setWearHapticFeedback(enabled: Boolean) diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenterImpl.kt b/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenterImpl.kt index ba8e75414..bd22314ac 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenterImpl.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenterImpl.kt @@ -233,12 +233,20 @@ class HomePresenterImpl @Inject constructor( return serverManager.webSocketRepository().getEntityRegistryUpdates() } - override suspend fun getTileShortcuts(): List { - return wearPrefsRepository.getTileShortcuts().map { SimplifiedEntity(it) } + override suspend fun getAllTileShortcuts(): Map> { + return wearPrefsRepository.getAllTileShortcuts().mapValues { (_, entities) -> + entities.map { + SimplifiedEntity(it) + } + } } - override suspend fun setTileShortcuts(entities: List) { - wearPrefsRepository.setTileShortcuts(entities.map { it.entityString }) + override suspend fun getTileShortcuts(tileId: Int): List { + return wearPrefsRepository.getTileShortcutsAndSaveTileId(tileId).map { SimplifiedEntity(it) } + } + + override suspend fun setTileShortcuts(tileId: Int?, entities: List) { + wearPrefsRepository.setTileShortcuts(tileId, entities.map { it.entityString }) } override suspend fun getWearHapticFeedback(): Boolean { diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/MainViewModel.kt b/wear/src/main/java/io/homeassistant/companion/android/home/MainViewModel.kt index 863ce0789..9a1341d64 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/MainViewModel.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/MainViewModel.kt @@ -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() - private set + val shortcutEntitiesMap = mutableStateMapOf>() + var areas = mutableListOf() 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) } } } diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/views/HomeView.kt b/wear/src/main/java/io/homeassistant/companion/android/home/views/HomeView.kt index 3e879f41e..58126e6f1 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/views/HomeView.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/views/HomeView.kt @@ -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() } ) diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/views/SelectShortcutsTileView.kt b/wear/src/main/java/io/homeassistant/companion/android/home/views/SelectShortcutsTileView.kt new file mode 100644 index 000000000..4f5935c1e --- /dev/null +++ b/wear/src/main/java/io/homeassistant/companion/android/home/views/SelectShortcutsTileView.kt @@ -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, + 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 = {} + ) +} diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/views/SetTileShortcutsView.kt b/wear/src/main/java/io/homeassistant/companion/android/home/views/SetShortcutsTileView.kt similarity index 66% rename from wear/src/main/java/io/homeassistant/companion/android/home/views/SetTileShortcutsView.kt rename to wear/src/main/java/io/homeassistant/companion/android/home/views/SetShortcutsTileView.kt index 7d3476c15..373dba0f4 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/views/SetTileShortcutsView.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/views/SetShortcutsTileView.kt @@ -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, - onShortcutEntitySelectionChange: (Int) -> Unit, - isShowShortcutTextEnabled: Boolean, - onShowShortcutTextEnabled: (Boolean) -> Unit +fun SetShortcutsTileView( + shortcutEntities: List, + 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 = {} ) } diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/views/SettingsView.kt b/wear/src/main/java/io/homeassistant/companion/android/home/views/SettingsView.kt index 689c2ea66..2aea27ad8 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/views/SettingsView.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/views/SettingsView.kt @@ -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 ) } diff --git a/wear/src/main/java/io/homeassistant/companion/android/onboarding/integration/MobileAppIntegrationPresenterImpl.kt b/wear/src/main/java/io/homeassistant/companion/android/onboarding/integration/MobileAppIntegrationPresenterImpl.kt index 67d86ea70..15c5bcc7d 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/onboarding/integration/MobileAppIntegrationPresenterImpl.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/onboarding/integration/MobileAppIntegrationPresenterImpl.kt @@ -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) diff --git a/wear/src/main/java/io/homeassistant/companion/android/tiles/OpenShortcutTileSettingsActivity.kt b/wear/src/main/java/io/homeassistant/companion/android/tiles/OpenShortcutTileSettingsActivity.kt new file mode 100644 index 000000000..cee60dcbf --- /dev/null +++ b/wear/src/main/java/io/homeassistant/companion/android/tiles/OpenShortcutTileSettingsActivity.kt @@ -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() + } +} diff --git a/wear/src/main/java/io/homeassistant/companion/android/tiles/ShortcutsTile.kt b/wear/src/main/java/io/homeassistant/companion/android/tiles/ShortcutsTile.kt index 2c8f5b2ff..1457b2d25 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/tiles/ShortcutsTile.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/tiles/ShortcutsTile.kt @@ -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 { - return wearPrefsRepository.getTileShortcuts().map { SimplifiedEntity(it) } + private suspend fun getEntities(tileId: Int): List { + 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) + } + } }