mirror of
https://github.com/home-assistant/android
synced 2024-10-02 22:34:46 +00:00
Wear OS shortcut Tile (#1842)
* Add non-functional example of favorites tile * Load real scene entities into the Tile * Make the tile buttons actionable * Add icons of the entities * Add tile preview image * Also support fewer than 7 entities * Cleanup and pass ktlint formatting * Add settings page for tile shortcuts * Use new settings in Tile * Make the tile update when the settings are changed * Support all types of entities in TileActionActivity * Rename tile and process comments * ktlint * Update layout of settings a bit * Use a string resource like a normal person * Remove remaining SetTitle instances * Process review comments and add data class to store entity strings * Process review comments * tiny ktlint fix * Fix broken previews * Fix white lines after merge * Move tile refresh to compose function. * Fix crash when missing friendly name or icon. * ktlint... Co-authored-by: Justin Bassett <bassett.justint@gmail.com>
This commit is contained in:
parent
55a9c51250
commit
3d909c621d
|
@ -34,6 +34,8 @@ interface IntegrationRepository {
|
|||
|
||||
suspend fun setWearHomeFavorites(favorites: Set<String>)
|
||||
suspend fun getWearHomeFavorites(): Set<String>
|
||||
suspend fun getTileShortcuts(): List<String>
|
||||
suspend fun setTileShortcuts(entities: List<String>)
|
||||
suspend fun setWearHapticFeedback(enabled: Boolean)
|
||||
suspend fun getWearHapticFeedback(): Boolean
|
||||
suspend fun setWearToastConfirmation(enabled: Boolean)
|
||||
|
|
|
@ -26,6 +26,7 @@ import io.homeassistant.companion.android.common.data.url.UrlRepository
|
|||
import io.homeassistant.companion.android.common.data.websocket.WebSocketRepository
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.GetConfigResponse
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import org.json.JSONArray
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
@ -57,6 +58,7 @@ class IntegrationRepositoryImpl @Inject constructor(
|
|||
|
||||
private const val PREF_CHECK_SENSOR_REGISTRATION_NEXT = "sensor_reg_last"
|
||||
private const val PREF_WEAR_HOME_FAVORITES = "wear_home_favorites"
|
||||
private const val PREF_TILE_SHORTCUTS = "tile_shortcuts_list"
|
||||
private const val PREF_WEAR_HAPTIC_FEEDBACK = "wear_haptic_feedback"
|
||||
private const val PREF_WEAR_TOAST_CONFIRMATION = "wear_toast_confirmation"
|
||||
private const val PREF_HA_VERSION = "ha_version"
|
||||
|
@ -354,6 +356,17 @@ class IntegrationRepositoryImpl @Inject constructor(
|
|||
return localStorage.getStringSet(PREF_WEAR_HOME_FAVORITES) ?: setOf()
|
||||
}
|
||||
|
||||
override suspend fun getTileShortcuts(): List<String> {
|
||||
val jsonArray = JSONArray(localStorage.getString(PREF_TILE_SHORTCUTS) ?: "[]")
|
||||
return List(jsonArray.length()) {
|
||||
jsonArray.getString(it)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setTileShortcuts(entities: List<String>) {
|
||||
localStorage.putString(PREF_TILE_SHORTCUTS, JSONArray(entities).toString())
|
||||
}
|
||||
|
||||
override suspend fun setWearHapticFeedback(enabled: Boolean) {
|
||||
localStorage.putBoolean(PREF_WEAR_HAPTIC_FEEDBACK, enabled)
|
||||
}
|
||||
|
|
|
@ -80,6 +80,7 @@ dependencies {
|
|||
implementation(project(":common"))
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.5.1")
|
||||
|
||||
implementation("com.google.android.material:material:1.4.0")
|
||||
|
||||
|
@ -103,4 +104,7 @@ dependencies {
|
|||
implementation("androidx.wear.compose:compose-foundation:1.0.0-alpha10")
|
||||
implementation("androidx.wear.compose:compose-material:1.0.0-alpha10")
|
||||
implementation("androidx.wear.compose:compose-navigation:1.0.0-alpha10")
|
||||
|
||||
implementation("com.google.guava:guava:31.0.1-android")
|
||||
implementation("androidx.wear.tiles:tiles:1.0.0")
|
||||
}
|
||||
|
|
|
@ -43,6 +43,24 @@
|
|||
|
||||
<!-- To show confirmations and failures -->
|
||||
<activity android:name="androidx.wear.activity.ConfirmationActivity" />
|
||||
|
||||
<!-- Tiles -->
|
||||
<service
|
||||
android:name=".tiles.ShortcutsTile"
|
||||
android:label="@string/shortcuts"
|
||||
android:description="@string/shortcuts_tile_description"
|
||||
android:permission="com.google.android.wearable.permission.BIND_TILE_PROVIDER"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.wear.tiles.action.BIND_TILE_PROVIDER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="androidx.wear.tiles.PREVIEW"
|
||||
android:resource="@drawable/favorite_entities_tile_example" />
|
||||
</service>
|
||||
<activity android:name=".tiles.TileActionActivity"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,16 @@
|
|||
package io.homeassistant.companion.android.data
|
||||
|
||||
data class SimplifiedEntity(
|
||||
var entityId: String,
|
||||
var friendlyName: String = entityId,
|
||||
var icon: String = ""
|
||||
) {
|
||||
constructor(entityString: String) : this(
|
||||
entityString.split(",")[0],
|
||||
entityString.split(",")[1],
|
||||
entityString.split(",")[2]
|
||||
)
|
||||
|
||||
val entityString: String
|
||||
get() = "$entityId,$friendlyName,$icon"
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package io.homeassistant.companion.android.home
|
||||
|
||||
import io.homeassistant.companion.android.common.data.integration.Entity
|
||||
import io.homeassistant.companion.android.data.SimplifiedEntity
|
||||
|
||||
interface HomePresenter {
|
||||
|
||||
|
@ -11,6 +12,8 @@ interface HomePresenter {
|
|||
suspend fun getEntities(): List<Entity<*>>
|
||||
suspend fun getWearHomeFavorites(): List<String>
|
||||
suspend fun setWearHomeFavorites(favorites: List<String>)
|
||||
suspend fun getTileShortcuts(): List<SimplifiedEntity>
|
||||
suspend fun setTileShortcuts(entities: List<SimplifiedEntity>)
|
||||
|
||||
suspend fun getWearHapticFeedback(): Boolean
|
||||
suspend fun setWearHapticFeedback(enabled: Boolean)
|
||||
|
|
|
@ -7,6 +7,7 @@ import io.homeassistant.companion.android.common.data.authentication.SessionStat
|
|||
import io.homeassistant.companion.android.common.data.integration.DeviceRegistration
|
||||
import io.homeassistant.companion.android.common.data.integration.Entity
|
||||
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
||||
import io.homeassistant.companion.android.data.SimplifiedEntity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
@ -107,6 +108,14 @@ class HomePresenterImpl @Inject constructor(
|
|||
integrationUseCase.setWearHomeFavorites(favorites.toSet())
|
||||
}
|
||||
|
||||
override suspend fun getTileShortcuts(): List<SimplifiedEntity> {
|
||||
return integrationUseCase.getTileShortcuts().map { SimplifiedEntity(it) }
|
||||
}
|
||||
|
||||
override suspend fun setTileShortcuts(entities: List<SimplifiedEntity>) {
|
||||
integrationUseCase.setTileShortcuts(entities.map { it.entityString })
|
||||
}
|
||||
|
||||
override suspend fun getWearHapticFeedback(): Boolean {
|
||||
return integrationUseCase.getWearHapticFeedback()
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.homeassistant.companion.android.common.data.integration.Entity
|
||||
import io.homeassistant.companion.android.data.SimplifiedEntity
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainViewModel : ViewModel() {
|
||||
|
@ -21,6 +22,8 @@ class MainViewModel : ViewModel() {
|
|||
private set
|
||||
var favoriteEntityIds = mutableStateListOf<String>()
|
||||
private set
|
||||
var shortcutEntities = mutableStateListOf<SimplifiedEntity>()
|
||||
private set
|
||||
var isHapticEnabled = mutableStateOf(false)
|
||||
private set
|
||||
var isToastEnabled = mutableStateOf(false)
|
||||
|
@ -29,6 +32,7 @@ class MainViewModel : ViewModel() {
|
|||
private fun loadEntities() {
|
||||
viewModelScope.launch {
|
||||
favoriteEntityIds.addAll(homePresenter.getWearHomeFavorites())
|
||||
shortcutEntities.addAll(homePresenter.getTileShortcuts())
|
||||
isHapticEnabled.value = homePresenter.getWearHapticFeedback()
|
||||
isToastEnabled.value = homePresenter.getWearToastConfirmation()
|
||||
entities.addAll(homePresenter.getEntities())
|
||||
|
@ -69,6 +73,26 @@ class MainViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
fun setTileShortcut(index: Int, entity: SimplifiedEntity) {
|
||||
viewModelScope.launch {
|
||||
if (index < shortcutEntities.size) {
|
||||
shortcutEntities[index] = entity
|
||||
} else {
|
||||
shortcutEntities.add(entity)
|
||||
}
|
||||
homePresenter.setTileShortcuts(shortcutEntities)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearTileShortcut(index: Int) {
|
||||
viewModelScope.launch {
|
||||
if (index < shortcutEntities.size) {
|
||||
shortcutEntities.removeAt(index)
|
||||
homePresenter.setTileShortcuts(shortcutEntities)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setHapticEnabled(enabled: Boolean) {
|
||||
viewModelScope.launch {
|
||||
homePresenter.setWearHapticFeedback(enabled)
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
package io.homeassistant.companion.android.home.views
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.wear.compose.material.Chip
|
||||
import androidx.wear.compose.material.ChipDefaults
|
||||
import androidx.wear.compose.material.ScalingLazyColumn
|
||||
import androidx.wear.compose.material.ScalingLazyListState
|
||||
import androidx.wear.compose.material.Text
|
||||
import androidx.wear.compose.material.rememberScalingLazyListState
|
||||
import com.mikepenz.iconics.compose.Image
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.common.data.integration.Entity
|
||||
import io.homeassistant.companion.android.data.SimplifiedEntity
|
||||
import io.homeassistant.companion.android.util.RotaryEventState
|
||||
import io.homeassistant.companion.android.util.getIcon
|
||||
|
||||
@Composable
|
||||
fun ChooseEntityView(
|
||||
validEntities: List<Entity<*>>,
|
||||
onNoneClicked: () -> Unit,
|
||||
onEntitySelected: (entity: SimplifiedEntity) -> Unit
|
||||
) {
|
||||
val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState()
|
||||
RotaryEventState(scrollState = scalingLazyListState)
|
||||
ScalingLazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
top = 40.dp,
|
||||
start = 8.dp,
|
||||
end = 8.dp,
|
||||
bottom = 40.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
state = scalingLazyListState
|
||||
) {
|
||||
item {
|
||||
ListHeader(id = R.string.shortcuts)
|
||||
}
|
||||
item {
|
||||
Chip(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
icon = { Image(asset = CommunityMaterial.Icon.cmd_delete) },
|
||||
label = { Text(stringResource(id = R.string.none)) },
|
||||
onClick = onNoneClicked,
|
||||
colors = ChipDefaults.primaryChipColors(
|
||||
contentColor = Color.Black
|
||||
)
|
||||
)
|
||||
}
|
||||
items(validEntities.size) { index ->
|
||||
val attributes = validEntities[index].attributes as Map<*, *>
|
||||
val iconBitmap = getIcon(
|
||||
attributes["icon"] as String?,
|
||||
validEntities[index].entityId.split(".")[0],
|
||||
LocalContext.current
|
||||
)
|
||||
Chip(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
icon = {
|
||||
Image(
|
||||
asset = iconBitmap ?: CommunityMaterial.Icon.cmd_cellphone,
|
||||
colorFilter = ColorFilter.tint(Color.White)
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = attributes["friendly_name"].toString(),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
enabled = validEntities[index].state != "unavailable",
|
||||
onClick = {
|
||||
onEntitySelected(
|
||||
SimplifiedEntity(
|
||||
validEntities[index].entityId,
|
||||
attributes["friendly_name"] as String? ?: validEntities[index].entityId,
|
||||
attributes["icon"] as String? ?: ""
|
||||
)
|
||||
)
|
||||
},
|
||||
colors = ChipDefaults.secondaryChipColors()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,12 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
@ -16,9 +21,11 @@ import androidx.wear.compose.material.Text
|
|||
import androidx.wear.compose.navigation.SwipeDismissableNavHost
|
||||
import androidx.wear.compose.navigation.composable
|
||||
import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
|
||||
import androidx.wear.tiles.TileService
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.home.HomePresenterImpl
|
||||
import io.homeassistant.companion.android.home.MainViewModel
|
||||
import io.homeassistant.companion.android.tiles.ShortcutsTile
|
||||
import io.homeassistant.companion.android.util.LocalRotaryEventDispatcher
|
||||
import io.homeassistant.companion.android.util.RotaryEventDispatcher
|
||||
import io.homeassistant.companion.android.util.RotaryEventHandlerSetup
|
||||
|
@ -27,12 +34,16 @@ import io.homeassistant.companion.android.util.setChipDefaults
|
|||
private const val SCREEN_LANDING = "landing"
|
||||
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"
|
||||
|
||||
@ExperimentalWearMaterialApi
|
||||
@Composable
|
||||
fun LoadHomePage(
|
||||
mainViewModel: MainViewModel
|
||||
) {
|
||||
var shortcutEntitySelectionIndex: Int by remember { mutableStateOf(0) }
|
||||
val context = LocalContext.current
|
||||
|
||||
val rotaryEventDispatcher = RotaryEventDispatcher()
|
||||
if (mainViewModel.entities.isNullOrEmpty() && mainViewModel.favoriteEntityIds.isNullOrEmpty()) {
|
||||
|
@ -78,6 +89,7 @@ fun LoadHomePage(
|
|||
mainViewModel.favoriteEntityIds,
|
||||
{ swipeDismissableNavController.navigate(SCREEN_SET_FAVORITES) },
|
||||
{ mainViewModel.clearFavorites() },
|
||||
{ swipeDismissableNavController.navigate(SCREEN_SET_TILE_SHORTCUTS) },
|
||||
mainViewModel.isHapticEnabled.value,
|
||||
mainViewModel.isToastEnabled.value,
|
||||
{ mainViewModel.setHapticEnabled(it) },
|
||||
|
@ -98,6 +110,31 @@ fun LoadHomePage(
|
|||
}
|
||||
}
|
||||
}
|
||||
composable(SCREEN_SET_TILE_SHORTCUTS) {
|
||||
SetTileShortcutsView(
|
||||
mainViewModel.shortcutEntities
|
||||
) {
|
||||
shortcutEntitySelectionIndex = it
|
||||
swipeDismissableNavController.navigate(SCREEN_SELECT_TILE_SHORTCUT)
|
||||
}
|
||||
}
|
||||
composable(SCREEN_SELECT_TILE_SHORTCUT) {
|
||||
val validEntities = mainViewModel.entities
|
||||
.filter { it.entityId.split(".")[0] in HomePresenterImpl.supportedDomains }
|
||||
ChooseEntityView(
|
||||
validEntities,
|
||||
{
|
||||
mainViewModel.clearTileShortcut(shortcutEntitySelectionIndex)
|
||||
TileService.getUpdater(context).requestUpdate(ShortcutsTile::class.java)
|
||||
swipeDismissableNavController.navigateUp()
|
||||
},
|
||||
{ entity ->
|
||||
mainViewModel.setTileShortcut(shortcutEntitySelectionIndex, entity)
|
||||
TileService.getUpdater(context).requestUpdate(ShortcutsTile::class.java)
|
||||
swipeDismissableNavController.navigateUp()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,12 +31,13 @@ fun ListHeader(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun ListHeader(id: Int) {
|
||||
fun ListHeader(id: Int, modifier: Modifier = Modifier) {
|
||||
ListHeader {
|
||||
Row {
|
||||
Text(
|
||||
text = stringResource(id = id),
|
||||
color = Color.White
|
||||
color = Color.White,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
package io.homeassistant.companion.android.home.views
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.ScalingLazyColumn
|
||||
import androidx.wear.compose.material.ScalingLazyListState
|
||||
import androidx.wear.compose.material.Text
|
||||
import androidx.wear.compose.material.rememberScalingLazyListState
|
||||
import com.mikepenz.iconics.compose.Image
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.data.SimplifiedEntity
|
||||
import io.homeassistant.companion.android.util.RotaryEventState
|
||||
import io.homeassistant.companion.android.util.getIcon
|
||||
|
||||
@Composable
|
||||
fun SetTileShortcutsView(
|
||||
shortcutEntities: MutableList<SimplifiedEntity>,
|
||||
onShortcutEntitySelectionChange: (Int) -> Unit
|
||||
) {
|
||||
|
||||
val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState()
|
||||
RotaryEventState(scrollState = scalingLazyListState)
|
||||
ScalingLazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
top = 40.dp,
|
||||
start = 8.dp,
|
||||
end = 8.dp,
|
||||
bottom = 40.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
state = scalingLazyListState
|
||||
) {
|
||||
item {
|
||||
ListHeader(id = R.string.shortcuts)
|
||||
}
|
||||
items(shortcutEntities.size) { index ->
|
||||
|
||||
val iconBitmap = getIcon(
|
||||
shortcutEntities[index].icon,
|
||||
shortcutEntities[index].entityId.split(".")[0],
|
||||
LocalContext.current
|
||||
)
|
||||
|
||||
Chip(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
icon = {
|
||||
Image(
|
||||
iconBitmap ?: CommunityMaterial.Icon.cmd_cellphone,
|
||||
colorFilter = ColorFilter.tint(Color.White)
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.shortcut_n, index + 1)
|
||||
)
|
||||
},
|
||||
secondaryLabel = {
|
||||
Text(
|
||||
text = shortcutEntities[index].friendlyName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
onClick = { onShortcutEntitySelectionChange(index) },
|
||||
colors = ChipDefaults.secondaryChipColors()
|
||||
)
|
||||
}
|
||||
if (shortcutEntities.size < 7) {
|
||||
item {
|
||||
Button(
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
onClick = { onShortcutEntitySelectionChange(shortcutEntities.size) },
|
||||
colors = ButtonDefaults.primaryButtonColors()
|
||||
) {
|
||||
Image(
|
||||
CommunityMaterial.Icon3.cmd_plus_thick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package io.homeassistant.companion.android.home.views
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
@ -35,6 +36,7 @@ fun SettingsView(
|
|||
favorites: List<String>,
|
||||
onClickSetFavorites: () -> Unit,
|
||||
onClearFavorites: () -> Unit,
|
||||
onClickSetShortcuts: () -> Unit,
|
||||
isHapticEnabled: Boolean,
|
||||
isToastEnabled: Boolean,
|
||||
onHapticEnabled: (Boolean) -> Unit,
|
||||
|
@ -42,27 +44,27 @@ fun SettingsView(
|
|||
) {
|
||||
val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState()
|
||||
RotaryEventState(scrollState = scalingLazyListState)
|
||||
|
||||
ScalingLazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
top = 10.dp,
|
||||
start = 5.dp,
|
||||
end = 5.dp,
|
||||
top = 40.dp,
|
||||
start = 8.dp,
|
||||
end = 8.dp,
|
||||
bottom = 40.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
state = scalingLazyListState
|
||||
) {
|
||||
item {
|
||||
ListHeader(id = R.string.settings)
|
||||
}
|
||||
|
||||
item {
|
||||
Chip(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 20.dp),
|
||||
.fillMaxWidth(),
|
||||
icon = {
|
||||
Image(asset = CommunityMaterial.Icon3.cmd_star)
|
||||
},
|
||||
|
@ -77,12 +79,10 @@ fun SettingsView(
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Chip(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 10.dp),
|
||||
.fillMaxWidth(),
|
||||
icon = {
|
||||
Image(asset = CommunityMaterial.Icon.cmd_delete)
|
||||
},
|
||||
|
@ -168,6 +168,32 @@ fun SettingsView(
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
ListHeader(
|
||||
id = R.string.tile_settings,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
}
|
||||
item {
|
||||
Chip(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
icon = {
|
||||
Image(asset = CommunityMaterial.Icon3.cmd_star_circle_outline)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.shortcuts)
|
||||
)
|
||||
},
|
||||
onClick = onClickSetShortcuts,
|
||||
colors = ChipDefaults.primaryChipColors(
|
||||
contentColor = Color.Black
|
||||
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -183,6 +209,7 @@ private fun PreviewSettingsView() {
|
|||
favorites = previewFavoritesList,
|
||||
onClickSetFavorites = { /*TODO*/ },
|
||||
onClearFavorites = {},
|
||||
onClickSetShortcuts = {},
|
||||
isHapticEnabled = true,
|
||||
isToastEnabled = false,
|
||||
{},
|
||||
|
|
|
@ -0,0 +1,214 @@
|
|||
package io.homeassistant.companion.android.tiles
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.wear.tiles.ActionBuilders
|
||||
import androidx.wear.tiles.ColorBuilders.argb
|
||||
import androidx.wear.tiles.DimensionBuilders.dp
|
||||
import androidx.wear.tiles.LayoutElementBuilders
|
||||
import androidx.wear.tiles.LayoutElementBuilders.Box
|
||||
import androidx.wear.tiles.LayoutElementBuilders.Column
|
||||
import androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER
|
||||
import androidx.wear.tiles.LayoutElementBuilders.Layout
|
||||
import androidx.wear.tiles.LayoutElementBuilders.LayoutElement
|
||||
import androidx.wear.tiles.LayoutElementBuilders.Row
|
||||
import androidx.wear.tiles.LayoutElementBuilders.Spacer
|
||||
import androidx.wear.tiles.ModifiersBuilders
|
||||
import androidx.wear.tiles.RequestBuilders.ResourcesRequest
|
||||
import androidx.wear.tiles.RequestBuilders.TileRequest
|
||||
import androidx.wear.tiles.ResourceBuilders
|
||||
import androidx.wear.tiles.ResourceBuilders.Resources
|
||||
import androidx.wear.tiles.TileBuilders.Tile
|
||||
import androidx.wear.tiles.TileService
|
||||
import androidx.wear.tiles.TimelineBuilders.Timeline
|
||||
import androidx.wear.tiles.TimelineBuilders.TimelineEntry
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.mikepenz.iconics.IconicsColor
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.utils.backgroundColor
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
|
||||
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
||||
import io.homeassistant.companion.android.data.SimplifiedEntity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.guava.future
|
||||
import java.nio.ByteBuffer
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val RESOURCES_VERSION = "1"
|
||||
|
||||
// Dimensions (dp)
|
||||
private const val CIRCLE_SIZE = 56f
|
||||
private const val ICON_SIZE = 48f * 0.7071f // square that fits in 48dp circle
|
||||
private const val SPACING = 8f
|
||||
|
||||
class ShortcutsTile : TileService() {
|
||||
private val serviceJob = Job()
|
||||
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
||||
|
||||
@Inject
|
||||
lateinit var integrationUseCase: IntegrationRepository
|
||||
|
||||
override fun onTileRequest(requestParams: TileRequest): ListenableFuture<Tile> =
|
||||
serviceScope.future {
|
||||
val entities = getEntities()
|
||||
|
||||
Tile.Builder()
|
||||
.setResourcesVersion(entities.toString())
|
||||
.setTimeline(
|
||||
Timeline.Builder().addTimelineEntry(
|
||||
TimelineEntry.Builder().setLayout(
|
||||
Layout.Builder().setRoot(
|
||||
layout(entities)
|
||||
).build()
|
||||
).build()
|
||||
).build()
|
||||
).build()
|
||||
}
|
||||
|
||||
override fun onResourcesRequest(requestParams: ResourcesRequest): ListenableFuture<Resources> =
|
||||
serviceScope.future {
|
||||
val density = requestParams.deviceParameters!!.screenDensity
|
||||
val iconSizePx = (ICON_SIZE * density).roundToInt()
|
||||
val entities = getEntities()
|
||||
|
||||
Resources.Builder()
|
||||
.setVersion(entities.toString())
|
||||
.apply {
|
||||
entities.map { entity ->
|
||||
// Find icon name
|
||||
val iconName: String = if (entity.icon.startsWith("mdi")) {
|
||||
entity.icon.split(":")[1]
|
||||
} else {
|
||||
"palette" // Default scene icon
|
||||
}
|
||||
|
||||
// Create Bitmap from icon name
|
||||
val iconBitmap = IconicsDrawable(this@ShortcutsTile, "cmd-$iconName").apply {
|
||||
colorInt = Color.WHITE
|
||||
sizeDp = ICON_SIZE.roundToInt()
|
||||
backgroundColor = IconicsColor.colorRes(R.color.colorOverlay)
|
||||
}.toBitmap(iconSizePx, iconSizePx, Bitmap.Config.RGB_565)
|
||||
|
||||
// Make array of bitmap
|
||||
val bitmapData = ByteBuffer.allocate(iconBitmap.byteCount).apply {
|
||||
iconBitmap.copyPixelsToBuffer(this)
|
||||
}.array()
|
||||
|
||||
// link the entity id to the bitmap data array
|
||||
entity.entityId to ResourceBuilders.ImageResource.Builder()
|
||||
.setInlineResource(
|
||||
ResourceBuilders.InlineImageResource.Builder()
|
||||
.setData(bitmapData)
|
||||
.setWidthPx(iconSizePx)
|
||||
.setHeightPx(iconSizePx)
|
||||
.setFormat(ResourceBuilders.IMAGE_FORMAT_RGB_565)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
}.forEach { (id, imageResource) ->
|
||||
addIdToImageMapping(id, imageResource)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
// Cleans up the coroutine
|
||||
serviceJob.cancel()
|
||||
}
|
||||
|
||||
private suspend fun getEntities(): List<SimplifiedEntity> {
|
||||
DaggerTilesComponent.builder()
|
||||
.appComponent((applicationContext as GraphComponentAccessor).appComponent)
|
||||
.build()
|
||||
.inject(this@ShortcutsTile)
|
||||
|
||||
return integrationUseCase.getTileShortcuts().map { SimplifiedEntity(it) }
|
||||
}
|
||||
|
||||
fun layout(entities: List<SimplifiedEntity>): LayoutElement = Column.Builder().apply {
|
||||
if (entities.isEmpty()) {
|
||||
addContent(
|
||||
LayoutElementBuilders.Text.Builder()
|
||||
.setText(getString(R.string.shortcuts_tile_empty))
|
||||
.build()
|
||||
)
|
||||
} else {
|
||||
addContent(rowLayout(entities.subList(0, min(2, entities.size))))
|
||||
if (entities.size > 2) {
|
||||
addContent(Spacer.Builder().setHeight(dp(SPACING)).build())
|
||||
addContent(rowLayout(entities.subList(2, min(5, entities.size))))
|
||||
}
|
||||
if (entities.size > 5) {
|
||||
addContent(Spacer.Builder().setHeight(dp(SPACING)).build())
|
||||
addContent(rowLayout(entities.subList(5, min(7, entities.size))))
|
||||
}
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
private fun rowLayout(entities: List<SimplifiedEntity>): LayoutElement = Row.Builder().apply {
|
||||
addContent(iconLayout(entities[0]))
|
||||
entities.drop(1).forEach { entity ->
|
||||
addContent(Spacer.Builder().setWidth(dp(SPACING)).build())
|
||||
addContent(iconLayout(entity))
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
private fun iconLayout(entity: SimplifiedEntity): LayoutElement = Box.Builder().apply {
|
||||
setWidth(dp(CIRCLE_SIZE))
|
||||
setHeight(dp(CIRCLE_SIZE))
|
||||
setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
|
||||
setModifiers(
|
||||
ModifiersBuilders.Modifiers.Builder()
|
||||
// Set circular background
|
||||
.setBackground(
|
||||
ModifiersBuilders.Background.Builder()
|
||||
.setColor(argb(ContextCompat.getColor(baseContext, R.color.colorOverlay)))
|
||||
.setCorner(
|
||||
ModifiersBuilders.Corner.Builder()
|
||||
.setRadius(dp(CIRCLE_SIZE / 2))
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
// Make clickable and call activity
|
||||
.setClickable(
|
||||
ModifiersBuilders.Clickable.Builder()
|
||||
.setOnClick(
|
||||
ActionBuilders.LaunchAction.Builder()
|
||||
.setAndroidActivity(
|
||||
ActionBuilders.AndroidActivity.Builder()
|
||||
.setClassName(TileActionActivity::class.java.name)
|
||||
.setPackageName(this@ShortcutsTile.packageName)
|
||||
.addKeyToExtraMapping("entity_id", ActionBuilders.stringExtra(entity.entityId))
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
addContent(
|
||||
// Add icon
|
||||
LayoutElementBuilders.Image.Builder()
|
||||
.setResourceId(entity.entityId)
|
||||
.setWidth(dp(ICON_SIZE))
|
||||
.setHeight(dp(ICON_SIZE))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
.build()
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package io.homeassistant.companion.android.tiles
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
|
||||
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
||||
import io.homeassistant.companion.android.home.HomePresenterImpl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class TileActionActivity : Activity() {
|
||||
|
||||
@Inject
|
||||
lateinit var integrationUseCase: IntegrationRepository
|
||||
|
||||
companion object {
|
||||
private const val TAG = "TileActionActivity"
|
||||
|
||||
fun newInstance(context: Context): Intent {
|
||||
return Intent(context, TileActionActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
DaggerTilesComponent
|
||||
.builder()
|
||||
.appComponent((application as GraphComponentAccessor).appComponent)
|
||||
.build()
|
||||
.inject(this)
|
||||
|
||||
val entityId: String? = intent.getStringExtra("entity_id")
|
||||
|
||||
if (entityId != null) {
|
||||
mainScope.launch {
|
||||
if (entityId.split(".")[0] in HomePresenterImpl.toggleDomains) {
|
||||
integrationUseCase.callService(
|
||||
entityId.split(".")[0],
|
||||
"toggle",
|
||||
hashMapOf("entity_id" to entityId)
|
||||
)
|
||||
} else {
|
||||
integrationUseCase.callService(
|
||||
entityId.split(".")[0],
|
||||
"turn_on",
|
||||
hashMapOf("entity_id" to entityId)
|
||||
)
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
// Cleans up the coroutine
|
||||
mainScope.cancel()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package io.homeassistant.companion.android.tiles
|
||||
|
||||
import dagger.Component
|
||||
import io.homeassistant.companion.android.common.dagger.AppComponent
|
||||
|
||||
@Component(dependencies = [AppComponent::class])
|
||||
interface TilesComponent {
|
||||
|
||||
fun inject(shortcutsTile: ShortcutsTile)
|
||||
|
||||
fun inject(tileActionActivity: TileActionActivity)
|
||||
}
|
BIN
wear/src/main/res/drawable/favorite_entities_tile_example.png
Normal file
BIN
wear/src/main/res/drawable/favorite_entities_tile_example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
|
@ -22,12 +22,18 @@
|
|||
<string name="login">Login</string>
|
||||
<string name="logout">Logout</string>
|
||||
<string name="manual_setup">Manual setup</string>
|
||||
<string name="none">None</string>
|
||||
<string name="other">Other</string>
|
||||
<string name="password">Password</string>
|
||||
<string name="registerDevice">Register watch</string>
|
||||
<string name="scene">Scene</string>
|
||||
<string name="scenes">Scenes</string>
|
||||
<string name="scripts">Scripts</string>
|
||||
<string name="shortcuts">Shortcuts</string>
|
||||
<string name="shortcut_n">Shortcut %d</string>
|
||||
<string name="shortcuts_tile_description">Select up to 7 entities</string>
|
||||
<string name="shortcuts_tile_empty">Choose entities in settings</string>
|
||||
<string name="tile_settings">Tile settings</string>
|
||||
<string name="username">Username</string>
|
||||
<string name="version">Version: %s</string>
|
||||
<string name="lights">Lights</string>
|
||||
|
|
Loading…
Reference in a new issue