Allow favorite entities to be dragged and dropped for reordering from phone settings (#1963)

* Allow favorite entities to be dragged and dropped for reordering

* Switch favorites to store in DB, fix reordering

* UI updates to reflect draggable items

* Send a json array instead of just a string, add lock domain to settings

* Only add favorites if we have them from the phone

* Remove missing entities, fix initial duplicate load

* Remove redundant work manager bump from phone

* Fix flow of favorites and remaining issues

* Build fixes

* Switch back checked variable
This commit is contained in:
Daniel Shokouhi 2021-12-08 17:52:51 -08:00 committed by GitHub
parent ab6cf02671
commit f800c85b52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 223 additions and 198 deletions

View file

@ -146,9 +146,9 @@ dependencies {
implementation("com.google.dagger:hilt-android:2.40.2")
kapt("com.google.dagger:hilt-android-compiler:2.40.2")
implementation("androidx.appcompat:appcompat:1.3.1")
implementation("androidx.appcompat:appcompat:1.4.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.2")
implementation("androidx.recyclerview:recyclerview:1.2.1")
implementation("androidx.preference:preference-ktx:1.1.1")
implementation("androidx.navigation:navigation-fragment-ktx:2.3.5")
@ -192,7 +192,9 @@ dependencies {
implementation("com.google.accompanist:accompanist-appcompat-theme:0.20.2")
implementation("com.mikepenz:iconics-core:5.3.3")
implementation("com.mikepenz:iconics-compose:5.3.3")
implementation("com.mikepenz:community-material-typeface:6.4.95.0-kotlin@aar")
implementation("org.burnoutcrew.composereorderable:reorderable:0.7.4")
}
// Disable to fix memory leak and be compatible with the configuration cache.

View file

@ -12,7 +12,6 @@ import com.google.android.gms.wearable.CapabilityClient
import com.google.android.gms.wearable.DataClient
import com.google.android.gms.wearable.DataEvent
import com.google.android.gms.wearable.DataEventBuffer
import com.google.android.gms.wearable.DataItem
import com.google.android.gms.wearable.DataMap
import com.google.android.gms.wearable.DataMapItem
import com.google.android.gms.wearable.PutDataMapRequest
@ -22,6 +21,9 @@ import io.homeassistant.companion.android.HomeAssistantApplication
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import kotlinx.coroutines.launch
import org.burnoutcrew.reorderable.ItemPosition
import org.burnoutcrew.reorderable.move
import org.json.JSONArray
import javax.inject.Inject
import io.homeassistant.companion.android.common.R as commonR
@ -35,8 +37,8 @@ class SettingsWearViewModel @Inject constructor(
private val application = getApplication<HomeAssistantApplication>()
companion object {
private val TAG = "SettingsWearViewModel"
private val CAPABILITY_WEAR_FAVORITES = "send_home_favorites"
private const val TAG = "SettingsWearViewModel"
private const val CAPABILITY_WEAR_FAVORITES = "send_home_favorites"
}
var entities = mutableStateMapOf<String, Entity<*>>()
@ -55,12 +57,15 @@ class SettingsWearViewModel @Inject constructor(
}
}
private fun saveHomeFavorites(data: String, item: DataItem) {
getFavorites(DataMapItem.fromDataItem(item).dataMap)
private fun saveHomeFavorites(data: String) {
val jsonString = JSONArray(data)
Log.d(TAG, "saveHomeFavorites: $jsonString")
favoriteEntityIds.clear()
favoriteEntityIds.addAll(
data.removeSurrounding("[", "]").split(", ").map { it }
)
for (favorite in 0 until jsonString.length()) {
favoriteEntityIds.add(
jsonString.getString(favorite)
)
}
}
fun onEntitySelected(checked: Boolean, entityId: String) {
@ -71,7 +76,13 @@ class SettingsWearViewModel @Inject constructor(
sendHomeFavorites(favoriteEntityIds.toList())
}
private fun sendHomeFavorites(favoritesList: List<String>) = viewModelScope.launch {
fun onMove(fromItem: ItemPosition, toItem: ItemPosition) {
favoriteEntityIds.move(favoriteEntityIds.indexOfFirst { it == fromItem.key }, favoriteEntityIds.indexOfFirst { it == toItem.key })
}
fun canDragOver(position: ItemPosition) = favoriteEntityIds.any { it == position.key }
fun sendHomeFavorites(favoritesList: List<String>) = viewModelScope.launch {
Log.d(TAG, "sendHomeFavorites")
val putDataRequest = PutDataMapRequest.create("/save_home_favorites").run {
@ -97,12 +108,9 @@ class SettingsWearViewModel @Inject constructor(
Log.d(TAG, "Found existing favorites: ${dataItemBuffer.count}")
dataItemBuffer.forEach {
val data = getFavorites(DataMapItem.fromDataItem(it).dataMap)
Log.d(TAG, "Favorites: $data")
favoriteEntityIds.clear()
favoriteEntityIds.addAll(
data.removeSurrounding("[", "]").split(", ").map { it }
)
saveHomeFavorites(data)
}
dataItemBuffer.release()
}
}
}
@ -156,11 +164,11 @@ class SettingsWearViewModel @Inject constructor(
event.dataItem.also { item ->
if (item.uri.path?.compareTo("/home_favorites") == 0) {
val data = getFavorites(DataMapItem.fromDataItem(item).dataMap)
saveHomeFavorites(data, item)
Log.d(TAG, "onDataChanged: Found home favorites: $data")
saveHomeFavorites(data)
}
}
}
}
dataEvents.release()
}
}

View file

@ -3,11 +3,13 @@ package io.homeassistant.companion.android.settings.views
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Checkbox
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Scaffold
@ -17,16 +19,22 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HelpOutline
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.mikepenz.iconics.IconicsDrawable
import io.homeassistant.companion.android.settings.SettingsWearViewModel
import org.burnoutcrew.reorderable.detectReorderAfterLongPress
import org.burnoutcrew.reorderable.draggedItem
import org.burnoutcrew.reorderable.rememberReorderState
import org.burnoutcrew.reorderable.reorderable
import io.homeassistant.companion.android.common.R as commonR
const val WEAR_DOCS_LINK = "https://companion.home-assistant.io/docs/wear-os/wear-os"
val supportedDomains = listOf(
"input_boolean", "light", "switch", "script", "scene"
"input_boolean", "light", "lock", "switch", "script", "scene"
)
@Composable
@ -34,6 +42,7 @@ fun LoadWearFavoritesSettings(
settingsWearViewModel: SettingsWearViewModel
) {
val context = LocalContext.current
val reorderState = rememberReorderState()
val validEntities = settingsWearViewModel.entities.filter { it.key.split(".")[0] in supportedDomains }.values.sortedBy { it.entityId }.toList()
val favoriteEntities = settingsWearViewModel.favoriteEntityIds
@ -56,8 +65,20 @@ fun LoadWearFavoritesSettings(
}
) {
LazyColumn(
state = reorderState.listState,
verticalArrangement = Arrangement.Center,
modifier = Modifier.padding(top = 10.dp, start = 20.dp, end = 20.dp)
modifier = Modifier
.padding(top = 10.dp, start = 5.dp, end = 10.dp)
.then(
Modifier.reorderable(
reorderState,
{ from, to -> settingsWearViewModel.onMove(from, to) },
canDragOver = { settingsWearViewModel.canDragOver(it) },
onDragEnd = { _, _ ->
settingsWearViewModel.sendHomeFavorites(settingsWearViewModel.favoriteEntityIds.toList())
}
)
)
) {
item {
Text(
@ -65,17 +86,24 @@ fun LoadWearFavoritesSettings(
fontWeight = FontWeight.Bold
)
}
items(favoriteEntities.size) { index ->
items(favoriteEntities.size, { favoriteEntities[it] }) { index ->
Row(
modifier = Modifier
.padding(15.dp)
.padding(12.dp)
.clickable {
settingsWearViewModel.onEntitySelected(
false,
favoriteEntities[index]
)
}
.draggedItem(
reorderState.offsetByKey(favoriteEntities[index]),
Orientation.Vertical
)
.detectReorderAfterLongPress(reorderState)
) {
val iconBitmap = IconicsDrawable(LocalContext.current, "cmd-drag_vertical").toBitmap().asImageBitmap()
Icon(iconBitmap, "", modifier = Modifier.padding(top = 13.dp))
Checkbox(
checked = favoriteEntities.contains(favoriteEntities[index]),
onCheckedChange = {
@ -89,6 +117,9 @@ fun LoadWearFavoritesSettings(
)
}
}
item {
Divider()
}
if (!validEntities.isNullOrEmpty()) {
items(validEntities.size) { index ->
val item = validEntities[index]

View file

@ -59,7 +59,7 @@ dependencies {
api("androidx.room:room-ktx:2.3.0")
kapt("androidx.room:room-compiler:2.3.0")
api("androidx.work:work-runtime-ktx:2.7.0")
api("androidx.work:work-runtime-ktx:2.7.1")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-jackson:2.9.0")

View file

@ -33,8 +33,6 @@ interface IntegrationRepository {
suspend fun setSessionExpireMillis(value: Long)
suspend fun getSessionExpireMillis(): Long
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)

View file

@ -59,7 +59,6 @@ class IntegrationRepositoryImpl @Inject constructor(
private const val PREF_SECRET = "secret"
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"
@ -350,14 +349,6 @@ class IntegrationRepositoryImpl @Inject constructor(
return localStorage.getLong(PREF_SESSION_EXPIRE) ?: 0
}
override suspend fun setWearHomeFavorites(favorites: Set<String>) {
localStorage.putStringSet(PREF_WEAR_HOME_FAVORITES, favorites)
}
override suspend fun getWearHomeFavorites(): Set<String> {
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()) {

View file

@ -30,6 +30,8 @@ import io.homeassistant.companion.android.database.sensor.EntriesTypeConverter
import io.homeassistant.companion.android.database.sensor.Sensor
import io.homeassistant.companion.android.database.sensor.SensorDao
import io.homeassistant.companion.android.database.sensor.Setting
import io.homeassistant.companion.android.database.wear.Favorites
import io.homeassistant.companion.android.database.wear.FavoritesDao
import io.homeassistant.companion.android.database.widget.ButtonWidgetDao
import io.homeassistant.companion.android.database.widget.ButtonWidgetEntity
import io.homeassistant.companion.android.database.widget.CameraWidgetDao
@ -55,9 +57,10 @@ import io.homeassistant.companion.android.common.R as commonR
StaticWidgetEntity::class,
TemplateWidgetEntity::class,
NotificationItem::class,
TileEntity::class
TileEntity::class,
Favorites::class
],
version = 19,
version = 20,
exportSchema = false
)
@TypeConverters(EntriesTypeConverter::class)
@ -71,6 +74,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun templateWidgetDao(): TemplateWidgetDao
abstract fun notificationDao(): NotificationDao
abstract fun tileDao(): TileDao
abstract fun favoritesDao(): FavoritesDao
companion object {
private const val DATABASE_NAME = "HomeAssistantDB"
@ -113,7 +117,8 @@ abstract class AppDatabase : RoomDatabase() {
MIGRATION_15_16,
MIGRATION_16_17,
MIGRATION_17_18,
MIGRATION_18_19
MIGRATION_18_19,
MIGRATION_19_20
)
.fallbackToDestructiveMigration()
.build()
@ -442,6 +447,12 @@ abstract class AppDatabase : RoomDatabase() {
}
}
private val MIGRATION_19_20 = object : Migration(19, 20) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `favorites` (`id` TEXT PRIMARY KEY NOT NULL, `position` INTEGER)")
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

View file

@ -0,0 +1,14 @@
package io.homeassistant.companion.android.database.wear
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "favorites")
data class Favorites(
@PrimaryKey
@ColumnInfo(name = "id")
var id: String,
@ColumnInfo(name = "position")
var position: Int?
)

View file

@ -0,0 +1,33 @@
package io.homeassistant.companion.android.database.wear
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface FavoritesDao {
@Query("SELECT * FROM favorites where id = :id")
fun get(id: String): Favorites?
@Query("SELECT * FROM favorites ORDER BY position ASC")
fun getAllFlow(): Flow<List<Favorites>>?
@Query("SELECT * FROM favorites ORDER BY position ASC")
fun getAll(): List<Favorites>?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun add(favorite: Favorites)
@Update
fun update(favorite: Favorites)
@Query("DELETE FROM favorites where id = :id")
fun delete(id: String)
@Query("DELETE FROM favorites")
fun deleteAll()
}

View file

@ -644,7 +644,7 @@
<string name="wear_os_category">Wear OS</string>
<string name="wear_os_settings_summary">Manage Wear OS App</string>
<string name="wear_os_settings_title">Wear OS Settings</string>
<string name="wear_set_favorites">Select your favorite entities to appear at the top of the wear home screen</string>
<string name="wear_set_favorites">Select your favorite entities to appear at the top of the wear home screen. You can also drag and drop to change the order in which they appear.</string>
<string name="wear_settings">Wear Device Settings</string>
<string name="webview_error_AUTH_SCHEME">Unsupported authentication scheme (not basic or digest), please check network settings.</string>
<string name="webview_error_AUTHENTICATION">User authentication failed on server, please check server settings.</string>

View file

@ -48,7 +48,6 @@ class HomeActivity : ComponentActivity(), HomeView {
}
override fun onResume() {
mainViewModel.updateFavorites()
super.onResume()
SensorWorker.start(this)

View file

@ -17,8 +17,6 @@ interface HomePresenter {
suspend fun getEntities(): List<Entity<*>>
suspend fun getEntityUpdates(): Flow<Entity<*>>
suspend fun getWearHomeFavorites(): List<String>
suspend fun setWearHomeFavorites(favorites: List<String>)
suspend fun getTileShortcuts(): List<SimplifiedEntity>
suspend fun setTileShortcuts(entities: List<SimplifiedEntity>)

View file

@ -117,14 +117,6 @@ class HomePresenterImpl @Inject constructor(
return integrationUseCase.isRegistered()
}
override suspend fun getWearHomeFavorites(): List<String> {
return integrationUseCase.getWearHomeFavorites().toList()
}
override suspend fun setWearHomeFavorites(favorites: List<String>) {
integrationUseCase.setWearHomeFavorites(favorites.toSet())
}
override suspend fun getTileShortcuts(): List<SimplifiedEntity> {
return integrationUseCase.getTileShortcuts().map { SimplifiedEntity(it) }
}

View file

@ -1,26 +1,34 @@
package io.homeassistant.companion.android.home
import android.app.Application
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import io.homeassistant.companion.android.HomeAssistantApplication
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.data.SimplifiedEntity
import io.homeassistant.companion.android.database.AppDatabase
import io.homeassistant.companion.android.database.wear.Favorites
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor() : ViewModel() {
class MainViewModel @Inject constructor(application: Application) : AndroidViewModel(application) {
private lateinit var homePresenter: HomePresenter
val app = getApplication<HomeAssistantApplication>()
private val favoritesDao = AppDatabase.getInstance(app.applicationContext).favoritesDao()
// TODO: This is bad, do this instead: https://stackoverflow.com/questions/46283981/android-viewmodel-additional-arguments
fun init(homePresenter: HomePresenter) {
this.homePresenter = homePresenter
loadEntities()
getFavorites()
}
// entities
@ -52,12 +60,13 @@ class MainViewModel @Inject constructor() : ViewModel() {
var isToastEnabled = mutableStateOf(false)
private set
private fun favorites(): Flow<List<Favorites>>? = favoritesDao.getAllFlow()
private fun loadEntities() {
viewModelScope.launch {
if (!homePresenter.isConnected()) {
return@launch
}
favoriteEntityIds.addAll(homePresenter.getWearHomeFavorites())
shortcutEntities.addAll(homePresenter.getTileShortcuts())
isHapticEnabled.value = homePresenter.getWearHapticFeedback()
isToastEnabled.value = homePresenter.getWearToastConfirmation()
@ -94,35 +103,20 @@ class MainViewModel @Inject constructor() : ViewModel() {
}
}
fun addFavorite(entityId: String) {
private fun getFavorites() {
viewModelScope.launch {
favoriteEntityIds.add(entityId)
homePresenter.setWearHomeFavorites(favoriteEntityIds)
}
}
fun removeFavorite(entity: String) {
viewModelScope.launch {
favoriteEntityIds.remove(entity)
homePresenter.setWearHomeFavorites(favoriteEntityIds)
favorites()?.collect {
favoriteEntityIds.clear()
for (favorite in it) {
favoriteEntityIds.add(favorite.id)
}
}
}
}
fun clearFavorites() {
viewModelScope.launch {
favoriteEntityIds.clear()
homePresenter.setWearHomeFavorites(favoriteEntityIds)
}
}
// TODO: Remove the below as we should save favorites to the DB so we can use a proper flow like above
fun updateFavorites() {
viewModelScope.launch {
favoriteEntityIds.clear()
favoriteEntityIds.addAll(homePresenter.getWearHomeFavorites())
}
favoriteEntityIds.clear()
favoritesDao.deleteAll()
}
fun setTileShortcut(index: Int, entity: SimplifiedEntity) {
@ -159,6 +153,32 @@ class MainViewModel @Inject constructor() : ViewModel() {
}
}
fun addFavorites(favorites: Favorites) {
favoritesDao.add(favorites)
updateFavoritePositions()
}
private fun updateFavorites(favorites: Favorites) {
favoritesDao.update(favorites)
updateFavoritePositions()
}
fun removeFavorites(id: String) {
favoritesDao.delete(id)
updateFavoritePositions()
}
private fun updateFavoritePositions() {
var i = 1
viewModelScope.launch {
favoritesDao.getAll()?.forEach { favorites ->
if (i != i)
updateFavorites(Favorites(favorites.id, i))
i++
}
}
}
fun logout() {
homePresenter.onLogoutClicked()
}

View file

@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
@ -18,7 +17,6 @@ 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
@ -32,9 +30,6 @@ import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.data.SimplifiedEntity
import io.homeassistant.companion.android.home.MainViewModel
import io.homeassistant.companion.android.theme.WearAppTheme
import io.homeassistant.companion.android.util.LocalRotaryEventDispatcher
import io.homeassistant.companion.android.util.RotaryEventDispatcher
import io.homeassistant.companion.android.util.RotaryEventHandlerSetup
import io.homeassistant.companion.android.util.RotaryEventState
import io.homeassistant.companion.android.util.getIcon
import io.homeassistant.companion.android.common.R as commonR
@ -238,19 +233,3 @@ private fun ChooseEntityChip(
colors = ChipDefaults.secondaryChipColors()
)
}
@Preview
@Composable
private fun PreviewChooseEntityView() {
val rotaryEventDispatcher = RotaryEventDispatcher()
CompositionLocalProvider(
LocalRotaryEventDispatcher provides rotaryEventDispatcher
) {
RotaryEventHandlerSetup(rotaryEventDispatcher)
ChooseEntityView(
mainViewModel = MainViewModel(),
onNoneClicked = { /*TODO*/ },
onEntitySelected = {}
)
}
}

View file

@ -13,7 +13,6 @@ 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
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
@ -23,6 +22,7 @@ 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.database.wear.Favorites
import io.homeassistant.companion.android.home.MainViewModel
import io.homeassistant.companion.android.theme.WearAppTheme
import io.homeassistant.companion.android.tiles.ShortcutsTile
@ -88,7 +88,8 @@ fun LoadHomePage(
swipeDismissableNavController.navigate(SCREEN_ENTITY_LIST)
},
mainViewModel.isHapticEnabled.value,
mainViewModel.isToastEnabled.value
mainViewModel.isToastEnabled.value,
{ id -> mainViewModel.removeFavorites(id) }
)
}
composable(SCREEN_ENTITY_LIST) {
@ -119,11 +120,12 @@ fun LoadHomePage(
SetFavoritesView(
mainViewModel,
mainViewModel.favoriteEntityIds
) { entityId, isSelected ->
) { entityId, position, isSelected ->
val favorites = Favorites(entityId, position)
if (isSelected) {
mainViewModel.addFavorite(entityId)
mainViewModel.addFavorites(favorites)
} else {
mainViewModel.removeFavorite(entityId)
mainViewModel.removeFavorites(entityId)
}
}
}
@ -155,11 +157,3 @@ fun LoadHomePage(
}
}
}
@ExperimentalAnimationApi
@ExperimentalWearMaterialApi
@Preview
@Composable
private fun PreviewHomeView() {
LoadHomePage(mainViewModel = MainViewModel())
}

View file

@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
@ -22,7 +21,6 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
@ -39,12 +37,10 @@ import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.home.MainViewModel
import io.homeassistant.companion.android.theme.WearAppTheme
import io.homeassistant.companion.android.theme.wearColorPalette
import io.homeassistant.companion.android.util.LocalRotaryEventDispatcher
import io.homeassistant.companion.android.util.RotaryEventDispatcher
import io.homeassistant.companion.android.util.RotaryEventState
import io.homeassistant.companion.android.util.getIcon
import io.homeassistant.companion.android.util.onEntityClickedFeedback
import io.homeassistant.companion.android.util.previewFavoritesList
import io.homeassistant.companion.android.common.R as commonR
@ExperimentalAnimationApi
@ -57,7 +53,8 @@ fun MainView(
onSettingsClicked: () -> Unit,
onTestClicked: (entityLists: Map<Int, List<Entity<*>>>) -> Unit,
isHapticEnabled: Boolean,
isToastEnabled: Boolean
isToastEnabled: Boolean,
deleteFavorite: (String) -> Unit
) {
val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState()
@ -125,12 +122,21 @@ fun MainView(
colors = ChipDefaults.secondaryChipColors()
)
} else {
EntityUi(
mainViewModel.entities[favoriteEntityID]!!,
onEntityClicked,
isHapticEnabled,
isToastEnabled
)
var isValidEntity = false
for (entity in mainViewModel.entities) {
if (entity.value.entityId == favoriteEntityID) {
isValidEntity = true
EntityUi(
mainViewModel.entities[favoriteEntityID]!!,
onEntityClicked,
isHapticEnabled,
isToastEnabled
)
}
}
if (!isValidEntity) {
deleteFavorite(favoriteEntityID)
}
}
}
}
@ -337,25 +343,3 @@ fun MainView(
}
}
}
@ExperimentalAnimationApi
@ExperimentalWearMaterialApi
@Preview
@Composable
private fun PreviewMainView() {
val rotaryEventDispatcher = RotaryEventDispatcher()
CompositionLocalProvider(
LocalRotaryEventDispatcher provides rotaryEventDispatcher
) {
MainView(
mainViewModel = MainViewModel(),
favoriteEntityIds = previewFavoritesList,
onEntityClicked = { _, _ -> },
onSettingsClicked = {},
onTestClicked = {},
isHapticEnabled = true,
isToastEnabled = false
)
}
}

View file

@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
@ -15,7 +14,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.ExperimentalWearMaterialApi
import androidx.wear.compose.material.PositionIndicator
@ -29,16 +27,11 @@ 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.common.data.integration.Entity
import io.homeassistant.companion.android.home.HomePresenterImpl
import io.homeassistant.companion.android.home.MainViewModel
import io.homeassistant.companion.android.theme.WearAppTheme
import io.homeassistant.companion.android.theme.wearColorPalette
import io.homeassistant.companion.android.util.LocalRotaryEventDispatcher
import io.homeassistant.companion.android.util.RotaryEventDispatcher
import io.homeassistant.companion.android.util.RotaryEventHandlerSetup
import io.homeassistant.companion.android.util.RotaryEventState
import io.homeassistant.companion.android.util.getIcon
import io.homeassistant.companion.android.util.previewFavoritesList
import io.homeassistant.companion.android.common.R as commonR
@ExperimentalAnimationApi
@ -47,7 +40,7 @@ import io.homeassistant.companion.android.common.R as commonR
fun SetFavoritesView(
mainViewModel: MainViewModel,
favoriteEntityIds: List<String>,
onFavoriteSelected: (entityId: String, isSelected: Boolean) -> Unit
onFavoriteSelected: (entityId: String, entityPosition: Int, isSelected: Boolean) -> Unit
) {
var expandedInputBooleans: Boolean by rememberSaveable { mutableStateOf(true) }
var expandedLights: Boolean by rememberSaveable { mutableStateOf(true) }
@ -56,10 +49,6 @@ fun SetFavoritesView(
var expandedScripts: Boolean by rememberSaveable { mutableStateOf(true) }
var expandedSwitches: Boolean by rememberSaveable { mutableStateOf(true) }
val validEntities = mainViewModel.entities
.filter { it.key.split(".")[0] in HomePresenterImpl.supportedDomains }
val validEntityList = validEntities.values.toList().sortedBy { it.entityId }
val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState()
RotaryEventState(scrollState = scalingLazyListState)
@ -86,21 +75,6 @@ fun SetFavoritesView(
item {
ListHeader(id = commonR.string.set_favorite)
}
if (favoriteEntityIds.isNotEmpty()) {
val favoriteEntities = mutableListOf<Entity<*>>()
for (entity in validEntityList) {
if (favoriteEntityIds.contains(entity.entityId))
favoriteEntities += listOf(entity)
}
items(favoriteEntities.size) { index ->
FavoriteToggleChip(
entityList = favoriteEntities,
index = index,
favoriteEntityIds = favoriteEntityIds,
onFavoriteSelected = onFavoriteSelected
)
}
}
if (mainViewModel.inputBooleans.isNotEmpty()) {
item {
ListHeader(
@ -230,7 +204,7 @@ private fun FavoriteToggleChip(
entityList: List<Entity<*>>,
index: Int,
favoriteEntityIds: List<String>,
onFavoriteSelected: (entityId: String, isSelected: Boolean) -> Unit
onFavoriteSelected: (entityId: String, entityPosition: Int, isSelected: Boolean) -> Unit
) {
val attributes = entityList[index].attributes as Map<*, *>
val iconBitmap = getIcon(
@ -244,7 +218,7 @@ private fun FavoriteToggleChip(
ToggleChip(
checked = checked,
onCheckedChange = {
onFavoriteSelected(entityId, it)
onFavoriteSelected(entityId, favoriteEntityIds.size + 1, it)
},
modifier = Modifier
.fillMaxWidth(),
@ -264,21 +238,3 @@ private fun FavoriteToggleChip(
toggleIcon = { ToggleChipDefaults.SwitchIcon(checked) }
)
}
@ExperimentalAnimationApi
@ExperimentalWearMaterialApi
@Preview
@Composable
private fun PreviewSetFavoriteView() {
val rotaryEventDispatcher = RotaryEventDispatcher()
CompositionLocalProvider(
LocalRotaryEventDispatcher provides rotaryEventDispatcher
) {
RotaryEventHandlerSetup(rotaryEventDispatcher)
SetFavoritesView(
mainViewModel = MainViewModel(),
favoriteEntityIds = previewFavoritesList,
onFavoriteSelected = { _, _ -> }
)
}
}

View file

@ -13,10 +13,13 @@ import com.google.android.gms.wearable.Wearable
import com.google.android.gms.wearable.WearableListenerService
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.database.AppDatabase
import io.homeassistant.companion.android.database.wear.Favorites
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.json.JSONArray
import javax.inject.Inject
@AndroidEntryPoint
@ -67,10 +70,19 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
private fun sendHomeFavorites(nodeId: String) = mainScope.launch {
Log.d(TAG, "sendHomeFavorites to: $nodeId")
val currentFavorites = integrationUseCase.getWearHomeFavorites().toList()
val currentFavorites = AppDatabase.getInstance(applicationContext).favoritesDao().getAll()
val list = emptyList<String>().toMutableList()
for (favorite in currentFavorites!!) {
list += listOf(favorite.id)
}
val jsonArray = JSONArray(list.toString())
val jsonString = List(jsonArray.length()) {
jsonArray.getString(it)
}.map { it }
Log.d(TAG, "new list: $jsonString")
val putDataRequest = PutDataMapRequest.create("/home_favorites").run {
dataMap.putString("favorites", currentFavorites.toString())
dataMap.putString("favorites", jsonString.toString())
setUrgent()
asPutDataRequest()
}
@ -85,22 +97,25 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
private fun saveFavorites() {
Log.d(TAG, "Finding existing favorites")
val favoritesDao = AppDatabase.getInstance(applicationContext).favoritesDao()
mainScope.launch {
Wearable.getDataClient(applicationContext).getDataItems(Uri.parse("wear://*/save_home_favorites"))
.addOnSuccessListener {
Log.d(TAG, "Found existing favorites: ${it.count}")
it.forEach { dataItem ->
val data = getHomeFavorites(DataMapItem.fromDataItem(dataItem).dataMap)
val data = getHomeFavorites(DataMapItem.fromDataItem(dataItem).dataMap).removeSurrounding("[", "]").split(", ").toList()
Log.d(
TAG,
"Favorites: ${data.removeSurrounding("[", "]").split(", ").map { it }}"
"Favorites: $data"
)
mainScope.launch {
integrationUseCase.setWearHomeFavorites(
data.removeSurrounding("[", "]").split(", ").map { it }.toSet()
)
favoritesDao.deleteAll()
if (data.isNotEmpty()) {
data.forEachIndexed { index, s ->
favoritesDao.add(Favorites(s, index))
}
}
}
it.release()
}
}
}