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 89b07c87a25..b2beb6b8409 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 fc8d8398fda..7f0bf459565 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 00000000000..23ea7b4d168 --- /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 ff938b5d8ae..9feb477dedf 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 33ded3396a3..e43c91b6b40 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 e16e8b678df..aa63f4c4776 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 938c14fe078..4e618370723 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 d0d5bf3d019..a99de612eb2 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 ba8e7541411..bd22314acee 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 getTileShortcuts(tileId: Int): List { + return wearPrefsRepository.getTileShortcutsAndSaveTileId(tileId).map { SimplifiedEntity(it) } } - override suspend fun setTileShortcuts(entities: List) { - wearPrefsRepository.setTileShortcuts(entities.map { it.entityString }) + 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 863ce0789a1..9a1341d646f 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 3e879f41e6b..58126e6f1cc 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 00000000000..4f5935c1e31 --- /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 7d3476c1559..373dba0f44b 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 689c2ea661b..2aea27ad876 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 67d86ea7041..15c5bcc7d36 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 00000000000..cee60dcbfa8 --- /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 2c8f5b2ffd9..1457b2d2500 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) + } + } }