From 12600ac9178292574231b76f6bf8bfb477e37f94 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi <dshokouhi@gmail.com> Date: Wed, 27 Oct 2021 14:58:20 -0700 Subject: [PATCH 01/11] Convert wear home activity to compose --- wear/build.gradle.kts | 16 +- .../companion/android/home/HomeActivity.kt | 201 ++++++++++++++---- .../companion/android/home/HomePresenter.kt | 5 +- .../android/home/HomePresenterImpl.kt | 39 ++-- .../companion/android/home/HomeView.kt | 3 - wear/src/main/res/values/strings.xml | 2 + 6 files changed, 202 insertions(+), 64 deletions(-) diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index e205c7a2dac..f17e190cc1b 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -10,7 +10,7 @@ android { defaultConfig { applicationId = "io.homeassistant.companion.android" - minSdk = 23 + minSdk = 25 targetSdk = 30 versionName = System.getenv("VERSION") ?: "LOCAL" @@ -26,6 +26,11 @@ android { buildFeatures { viewBinding = true + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.0.4" } compileOptions { @@ -74,7 +79,7 @@ play { dependencies { implementation(project(":common")) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2") implementation("com.google.android.material:material:1.4.0") @@ -89,4 +94,11 @@ dependencies { implementation("com.mikepenz:iconics-core:5.3.2") implementation("androidx.appcompat:appcompat:1.3.1") implementation("com.mikepenz:community-material-typeface:5.8.55.0-kotlin@aar") + implementation("com.mikepenz:iconics-compose:5.3.2") + + implementation("androidx.activity:activity-compose:1.3.1") + implementation("androidx.compose.compiler:compiler:1.0.4") + implementation("androidx.compose.foundation:foundation:1.0.4") + implementation("androidx.wear.compose:compose-foundation:1.0.0-alpha08") + implementation("androidx.wear.compose:compose-material:1.0.0-alpha08") } 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 9f6bfe4b8b2..2ab39a1cdf1 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 @@ -3,9 +3,33 @@ package io.homeassistant.companion.android.home import android.content.Context import android.content.Intent import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.wear.widget.WearableRecyclerView +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +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.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +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 com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.compose.Image +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import io.homeassistant.companion.android.DaggerPresenterComponent import io.homeassistant.companion.android.PresenterModule import io.homeassistant.companion.android.R @@ -13,15 +37,14 @@ import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor import io.homeassistant.companion.android.common.data.integration.Entity import io.homeassistant.companion.android.onboarding.OnboardingActivity import io.homeassistant.companion.android.onboarding.integration.MobileAppIntegrationActivity +import kotlinx.coroutines.runBlocking import javax.inject.Inject -class HomeActivity : AppCompatActivity(), HomeView { +class HomeActivity : ComponentActivity(), HomeView { @Inject lateinit var presenter: HomePresenter - private lateinit var adapter: HomeListAdapter - companion object { private const val TAG = "HomeActivity" @@ -33,8 +56,6 @@ class HomeActivity : AppCompatActivity(), HomeView { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_home) - DaggerPresenterComponent .builder() .appComponent((application as GraphComponentAccessor).appComponent) @@ -42,16 +63,11 @@ class HomeActivity : AppCompatActivity(), HomeView { .build() .inject(this) - adapter = HomeListAdapter() - adapter.onSceneClicked = { entity -> presenter.onEntityClicked(entity) } - adapter.onButtonClicked = { id -> presenter.onButtonClicked(id) } - - findViewById<WearableRecyclerView>(R.id.home_list)?.apply { - layoutManager = LinearLayoutManager(this@HomeActivity) - adapter = this@HomeActivity.adapter + if (presenter.onViewReady()) { + setContent { + LoadHomePage() + } } - - presenter.onViewReady() } override fun onDestroy() { @@ -59,27 +75,6 @@ class HomeActivity : AppCompatActivity(), HomeView { super.onDestroy() } - override fun showHomeList(scenes: List<Entity<Any>>, scripts: List<Entity<Any>>, lights: List<Entity<Any>>, covers: List<Entity<Any>>) { - adapter.scenes.clear() - adapter.scripts.clear() - adapter.lights.clear() - adapter.covers.clear() - - if (scenes.isNotEmpty()) - adapter.scenes.addAll(scenes) - - if (scripts.isNotEmpty()) - adapter.scripts.addAll(scripts) - - if (lights.isNotEmpty()) - adapter.lights.addAll(lights) - - if (covers.isNotEmpty()) - adapter.covers.addAll(covers) - - adapter.notifyDataSetChanged() - } - override fun displayOnBoarding() { val intent = OnboardingActivity.newInstance(this) startActivity(intent) @@ -91,4 +86,134 @@ class HomeActivity : AppCompatActivity(), HomeView { startActivity(intent) finish() } + + @Composable + private fun LoadHomePage() { + + val entities = runBlocking { + presenter.getEntities() + } + + val scenes = entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "scene" } + val scripts = entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "script" } + val lights = entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "light" } + val inputBooleans = entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "input_boolean" } + val switches = entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "switch" } + + ScalingLazyColumn( + modifier = Modifier + .fillMaxSize(), + contentPadding = PaddingValues( + top = 10.dp, + start = 10.dp, + end = 10.dp, + bottom = 40.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + state = ScalingLazyListState() + ) { + if (inputBooleans.isNotEmpty()) { + items(inputBooleans.size) { index -> + if (index == 0) + SetTitle(R.string.input_booleans) + SetEntityUI(inputBooleans[index], index) + } + } + if (lights.isNotEmpty()) { + items(lights.size) { index -> + if (index == 0) + SetTitle(R.string.lights) + SetEntityUI(lights[index], index) + } + } + if (scenes.isNotEmpty()) { + items(scenes.size) { index -> + if (index == 0) + SetTitle(R.string.scenes) + + SetEntityUI(scenes[index], index) + } + } + if (scripts.isNotEmpty()) { + items(scripts.size) { index -> + if (index == 0) + SetTitle(R.string.scripts) + SetEntityUI(scripts[index], index) + } + } + if (switches.isNotEmpty()) { + items(switches.size) { index -> + if (index == 0) + SetTitle(R.string.switches) + SetEntityUI(switches[index], index) + } + } + + items(1) { + Column { + SetTitle(R.string.other) + Button( + modifier = Modifier + .width(140.dp), + onClick = { presenter.onLogoutClicked() }, + colors = ButtonDefaults.primaryButtonColors(backgroundColor = Color.Red) + ) { + Row { + Image(asset = CommunityMaterial.Icon.cmd_exit_run) + SetTitle(R.string.logout) + } + } + } + } + } + } + + @Composable + private fun SetEntityUI(entity: Entity<Any>, index: Int) { + val attributes = entity.attributes as Map<String, String> + val iconBitmap = + if (attributes["icon"]?.startsWith("mdi") == true) { + val icon = attributes["icon"]!!.split(":")[1] + IconicsDrawable(baseContext, "cmd-$icon").icon + } else { + when (entity.entityId.split(".")[0]) { + "input_boolean", "switch" -> CommunityMaterial.Icon2.cmd_light_switch + "light" -> CommunityMaterial.Icon2.cmd_lightbulb + "script" -> CommunityMaterial.Icon3.cmd_script_text_outline + "scene" -> CommunityMaterial.Icon3.cmd_palette_outline + else -> CommunityMaterial.Icon.cmd_cellphone + } + } + + Chip( + modifier = Modifier + .width(140.dp) + .padding(top = if (index == 0) 30.dp else 10.dp), + icon = { + if (iconBitmap != null) { + Image(asset = iconBitmap) + } else + Image(asset = CommunityMaterial.Icon.cmd_cellphone) + }, + label = { + Text( + text = attributes["friendly_name"].toString() + ) + }, + onClick = { presenter.onEntityClicked(entity) }, + colors = ChipDefaults.primaryChipColors(backgroundColor = Color.Blue, iconTintColor = Color.White) + ) + } + + @Composable + private fun SetTitle(id: Int) { + Text( + text = stringResource(id = id), + textAlign = TextAlign.Center, + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(start = 15.dp) + ) + } } 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 bc68056f4e8..25d748f45b4 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 @@ -4,9 +4,10 @@ import io.homeassistant.companion.android.common.data.integration.Entity interface HomePresenter { - fun onViewReady() + fun onViewReady(): Boolean fun onEntityClicked(entity: Entity<Any>) - fun onButtonClicked(id: String) fun onLogoutClicked() fun onFinish() + + suspend fun getEntities(): Array<Entity<Any>> } 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 7cbbc9ef804..87cdc3fcf9f 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 @@ -12,6 +12,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import javax.inject.Inject class HomePresenterImpl @Inject constructor( @@ -21,29 +22,43 @@ class HomePresenterImpl @Inject constructor( ) : HomePresenter { companion object { + private val toggleDomains = listOf( + "cover", "fan", "humidifier", "input_boolean", "light", + "media_player", "remote", "siren", "switch" + ) const val TAG = "HomePresenter" } private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job()) - override fun onViewReady() { - mainScope.launch { + override fun onViewReady(): Boolean { + var isRegistered = false + runBlocking { val sessionValid = authenticationUseCase.getSessionState() == SessionState.CONNECTED if (sessionValid && integrationUseCase.isRegistered()) { resyncRegistration() - // We'll stay on HomeActivity, so start loading entities - processEntities(integrationUseCase.getEntities()) + isRegistered = true } else if (sessionValid) { view.displayMobileAppIntegration() } else { view.displayOnBoarding() } } + return isRegistered + } + + override suspend fun getEntities(): Array<Entity<Any>> { + return try { + integrationUseCase.getEntities() + } catch (e: Exception) { + Log.e(TAG, "Unable to get entities", e) + emptyArray() + } } override fun onEntityClicked(entity: Entity<Any>) { - if (entity.entityId.split(".")[0] == "light") { + if (entity.entityId.split(".")[0] in toggleDomains) { mainScope.launch { integrationUseCase.callService( entity.entityId.split(".")[0], @@ -62,12 +77,6 @@ class HomePresenterImpl @Inject constructor( } } - override fun onButtonClicked(id: String) { - if (id == HomeListAdapter.BUTTON_ID_LOGOUT) { - onLogoutClicked() - } - } - override fun onLogoutClicked() { mainScope.launch { authenticationUseCase.revokeSession() @@ -79,14 +88,6 @@ class HomePresenterImpl @Inject constructor( mainScope.cancel() } - private fun processEntities(entities: Array<Entity<Any>>) { - val scenes = entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "scene" } - val scripts = entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "script" } - val lights = entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "light" } - val covers = entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "cover" } - view.showHomeList(scenes, scripts, lights, covers) - } - private fun resyncRegistration() { mainScope.launch { try { diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/HomeView.kt b/wear/src/main/java/io/homeassistant/companion/android/home/HomeView.kt index 9f2d4d67afe..423c5c0ffbb 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/HomeView.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/HomeView.kt @@ -1,9 +1,6 @@ package io.homeassistant.companion.android.home -import io.homeassistant.companion.android.common.data.integration.Entity - interface HomeView { - fun showHomeList(scenes: List<Entity<Any>>, scripts: List<Entity<Any>>, lights: List<Entity<Any>>, covers: List<Entity<Any>>) fun displayOnBoarding() fun displayMobileAppIntegration() diff --git a/wear/src/main/res/values/strings.xml b/wear/src/main/res/values/strings.xml index 8e4a2837bdf..57bad2b4db1 100644 --- a/wear/src/main/res/values/strings.xml +++ b/wear/src/main/res/values/strings.xml @@ -31,4 +31,6 @@ <string name="username">Username</string> <string name="version">Version: %s</string> <string name="lights">Lights</string> + <string name="input_booleans">Input Booleans</string> + <string name="switches">Switches</string> </resources> \ No newline at end of file From a7286a07297c01fe8941f8a877c783d814543659 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi <dshokouhi@gmail.com> Date: Wed, 27 Oct 2021 15:36:55 -0700 Subject: [PATCH 02/11] Bump compose libraries and make UI consistent --- wear/build.gradle.kts | 4 ++-- .../io/homeassistant/companion/android/home/HomeActivity.kt | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index f17e190cc1b..f9f6e63195d 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -99,6 +99,6 @@ dependencies { implementation("androidx.activity:activity-compose:1.3.1") implementation("androidx.compose.compiler:compiler:1.0.4") implementation("androidx.compose.foundation:foundation:1.0.4") - implementation("androidx.wear.compose:compose-foundation:1.0.0-alpha08") - implementation("androidx.wear.compose:compose-material:1.0.0-alpha08") + implementation("androidx.wear.compose:compose-foundation:1.0.0-alpha09") + implementation("androidx.wear.compose:compose-material:1.0.0-alpha09") } 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 2ab39a1cdf1..79a3649ef97 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 @@ -15,6 +15,7 @@ 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.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -201,7 +202,7 @@ class HomeActivity : ComponentActivity(), HomeView { ) }, onClick = { presenter.onEntityClicked(entity) }, - colors = ChipDefaults.primaryChipColors(backgroundColor = Color.Blue, iconTintColor = Color.White) + colors = ChipDefaults.primaryChipColors(backgroundColor = colorResource(id = R.color.colorAccent), contentColor = Color.Black) ) } From 255c6181b9fe448db3e2bfed9171faafdbbcf17f Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi <dshokouhi@gmail.com> Date: Thu, 28 Oct 2021 10:14:06 -0700 Subject: [PATCH 03/11] UI fixes based on review comments --- .../companion/android/home/HomeActivity.kt | 166 ++++++++++-------- 1 file changed, 95 insertions(+), 71 deletions(-) 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 79a3649ef97..6691ad1aac6 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 @@ -7,10 +7,9 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -21,13 +20,15 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -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.MaterialTheme +import androidx.wear.compose.material.PositionIndicator +import androidx.wear.compose.material.Scaffold 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.IconicsDrawable import com.mikepenz.iconics.compose.Image import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial @@ -95,73 +96,96 @@ class HomeActivity : ComponentActivity(), HomeView { presenter.getEntities() } - val scenes = entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "scene" } - val scripts = entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "script" } - val lights = entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "light" } - val inputBooleans = entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "input_boolean" } - val switches = entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "switch" } - - ScalingLazyColumn( - modifier = Modifier - .fillMaxSize(), - contentPadding = PaddingValues( - top = 10.dp, - start = 10.dp, - end = 10.dp, - bottom = 40.dp - ), - horizontalAlignment = Alignment.CenterHorizontally, - state = ScalingLazyListState() - ) { - if (inputBooleans.isNotEmpty()) { - items(inputBooleans.size) { index -> - if (index == 0) - SetTitle(R.string.input_booleans) - SetEntityUI(inputBooleans[index], index) - } - } - if (lights.isNotEmpty()) { - items(lights.size) { index -> - if (index == 0) - SetTitle(R.string.lights) - SetEntityUI(lights[index], index) + val scenes = + entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "scene" } + val scripts = + entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "script" } + val lights = + entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "light" } + val inputBooleans = entities.sortedBy { it.entityId } + .filter { it.entityId.split(".")[0] == "input_boolean" } + val switches = + entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "switch" } + + val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState() + + MaterialTheme { + Scaffold( + positionIndicator = { + if (scalingLazyListState.isScrollInProgress) + PositionIndicator(scalingLazyListState = scalingLazyListState) } - } - if (scenes.isNotEmpty()) { - items(scenes.size) { index -> - if (index == 0) - SetTitle(R.string.scenes) + ) { + ScalingLazyColumn( + modifier = Modifier + .fillMaxSize(), + contentPadding = PaddingValues( + top = 10.dp, + start = 10.dp, + end = 10.dp, + bottom = 40.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + state = scalingLazyListState + ) { + if (inputBooleans.isNotEmpty()) { + items(inputBooleans.size) { index -> + if (index == 0) + SetTitle(R.string.input_booleans) + SetEntityUI(inputBooleans[index], index) + } + } + if (lights.isNotEmpty()) { + items(lights.size) { index -> + if (index == 0) + SetTitle(R.string.lights) + SetEntityUI(lights[index], index) + } + } + if (scenes.isNotEmpty()) { + items(scenes.size) { index -> + if (index == 0) + SetTitle(R.string.scenes) - SetEntityUI(scenes[index], index) - } - } - if (scripts.isNotEmpty()) { - items(scripts.size) { index -> - if (index == 0) - SetTitle(R.string.scripts) - SetEntityUI(scripts[index], index) - } - } - if (switches.isNotEmpty()) { - items(switches.size) { index -> - if (index == 0) - SetTitle(R.string.switches) - SetEntityUI(switches[index], index) - } - } + SetEntityUI(scenes[index], index) + } + } + if (scripts.isNotEmpty()) { + items(scripts.size) { index -> + if (index == 0) + SetTitle(R.string.scripts) + SetEntityUI(scripts[index], index) + } + } + if (switches.isNotEmpty()) { + items(switches.size) { index -> + if (index == 0) + SetTitle(R.string.switches) + SetEntityUI(switches[index], index) + } + } - items(1) { - Column { - SetTitle(R.string.other) - Button( - modifier = Modifier - .width(140.dp), - onClick = { presenter.onLogoutClicked() }, - colors = ButtonDefaults.primaryButtonColors(backgroundColor = Color.Red) - ) { - Row { - Image(asset = CommunityMaterial.Icon.cmd_exit_run) - SetTitle(R.string.logout) + item { + Column { + SetTitle(R.string.other) + Chip( + modifier = Modifier + .fillMaxWidth() + .padding(top = 10.dp), + icon = { + Image(asset = CommunityMaterial.Icon.cmd_exit_run) + }, + label = { + Text( + text = stringResource(id = R.string.logout) + ) + }, + onClick = { presenter.onLogoutClicked() }, + colors = ChipDefaults.primaryChipColors( + backgroundColor = Color.Red, + contentColor = Color.Black + ) + ) } } } @@ -188,7 +212,7 @@ class HomeActivity : ComponentActivity(), HomeView { Chip( modifier = Modifier - .width(140.dp) + .fillMaxWidth() .padding(top = if (index == 0) 30.dp else 10.dp), icon = { if (iconBitmap != null) { @@ -198,7 +222,7 @@ class HomeActivity : ComponentActivity(), HomeView { }, label = { Text( - text = attributes["friendly_name"].toString() + text = attributes["friendly_name"].toString().take(30) ) }, onClick = { presenter.onEntityClicked(entity) }, @@ -214,7 +238,7 @@ class HomeActivity : ComponentActivity(), HomeView { fontSize = 15.sp, fontWeight = FontWeight.Bold, modifier = Modifier - .padding(start = 15.dp) + .fillMaxWidth() ) } } From c6f6c15c44e7cd48aeaaf9f72ceb537ec1964587 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi <dshokouhi@gmail.com> Date: Thu, 28 Oct 2021 10:36:15 -0700 Subject: [PATCH 04/11] Use chip overflow and max lines instead of take --- .../io/homeassistant/companion/android/home/HomeActivity.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 6691ad1aac6..78ca06687de 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 @@ -18,6 +18,7 @@ import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.wear.compose.material.Chip @@ -222,7 +223,9 @@ class HomeActivity : ComponentActivity(), HomeView { }, label = { Text( - text = attributes["friendly_name"].toString().take(30) + text = attributes["friendly_name"].toString(), + maxLines = 2, + overflow = TextOverflow.Ellipsis ) }, onClick = { presenter.onEntityClicked(entity) }, From f29995873fb926ae9f0700a65a29917c65821f71 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi <dshokouhi@gmail.com> Date: Thu, 28 Oct 2021 10:51:16 -0700 Subject: [PATCH 05/11] Bump activity compose dependency in wear --- wear/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index f9f6e63195d..e040bc679c2 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -96,7 +96,7 @@ dependencies { implementation("com.mikepenz:community-material-typeface:5.8.55.0-kotlin@aar") implementation("com.mikepenz:iconics-compose:5.3.2") - implementation("androidx.activity:activity-compose:1.3.1") + implementation("androidx.activity:activity-compose:1.4.0") implementation("androidx.compose.compiler:compiler:1.0.4") implementation("androidx.compose.foundation:foundation:1.0.4") implementation("androidx.wear.compose:compose-foundation:1.0.0-alpha09") From e804e2301c035c4976accd8a438f27db172a752e Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi <dshokouhi@gmail.com> Date: Thu, 28 Oct 2021 13:17:11 -0700 Subject: [PATCH 06/11] Clean up old files --- .../companion/android/home/HomeListAdapter.kt | 130 ------------------ .../android/viewHolders/ButtonViewHolder.kt | 27 ---- .../viewHolders/EntityButtonViewHolder.kt | 58 -------- wear/src/main/res/layout/activity_home.xml | 12 -- 4 files changed, 227 deletions(-) delete mode 100644 wear/src/main/java/io/homeassistant/companion/android/home/HomeListAdapter.kt delete mode 100644 wear/src/main/java/io/homeassistant/companion/android/viewHolders/ButtonViewHolder.kt delete mode 100644 wear/src/main/java/io/homeassistant/companion/android/viewHolders/EntityButtonViewHolder.kt delete mode 100644 wear/src/main/res/layout/activity_home.xml diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/HomeListAdapter.kt b/wear/src/main/java/io/homeassistant/companion/android/home/HomeListAdapter.kt deleted file mode 100644 index 68e24e40efa..00000000000 --- a/wear/src/main/java/io/homeassistant/companion/android/home/HomeListAdapter.kt +++ /dev/null @@ -1,130 +0,0 @@ -package io.homeassistant.companion.android.home - -import android.util.Log -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.ViewHolder -import io.homeassistant.companion.android.R -import io.homeassistant.companion.android.common.data.integration.Entity -import io.homeassistant.companion.android.viewHolders.ButtonViewHolder -import io.homeassistant.companion.android.viewHolders.EntityButtonViewHolder -import io.homeassistant.companion.android.viewHolders.HeaderViewHolder -import io.homeassistant.companion.android.viewHolders.LoadingViewHolder -import kotlin.math.max - -class HomeListAdapter() : RecyclerView.Adapter<ViewHolder>() { - - lateinit var onSceneClicked: (Entity<Any>) -> Unit - lateinit var onButtonClicked: (String) -> Unit - - val scenes = arrayListOf<Entity<Any>>() - val scripts = arrayListOf<Entity<Any>>() - val lights = arrayListOf<Entity<Any>>() - val covers = arrayListOf<Entity<Any>>() - val dataList = mutableListOf<Entity<Any>>() - - companion object { - private const val TYPE_SCENE = 1 // Used for scenes and scripts - private const val TYPE_HEADER = 2 - private const val TYPE_LOADING = 3 - private const val TYPE_BUTTON = 4 - - const val BUTTON_ID_LOGOUT: String = "logout" - - private const val TAG = "HomeListAdapter" - } - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): ViewHolder { - return when (viewType) { - TYPE_SCENE -> { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.listitem_entity_button, parent, false) - EntityButtonViewHolder(view, onSceneClicked) - } - TYPE_HEADER -> { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.listitem_header, parent, false) - HeaderViewHolder(view) - } - TYPE_BUTTON -> { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.listitem_button, parent, false) - ButtonViewHolder(view, onButtonClicked) - } - else -> { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.listitem_loading, parent, false) - LoadingViewHolder(view) - } - } - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - try { - if (holder is EntityButtonViewHolder) { - if (position < scenes.size + 1) { - holder.entity = scenes[position - 1] - } else if (position > scenes.size + 1 + scripts.size + 1) { - holder.entity = lights[position - 3 - scenes.size - scripts.size] - } else - holder.entity = scripts[position - 2 - scenes.size] - } else if (holder is HeaderViewHolder) { - when (position) { - 0 -> holder.headerTextView.setText(R.string.scenes) - scenes.size + 1 -> holder.headerTextView.setText(R.string.scripts) - scenes.size + scripts.size + 2 -> holder.headerTextView.setText(R.string.lights) - else -> holder.headerTextView.setText(R.string.other) - } - } else if (holder is ButtonViewHolder) { - holder.txtName.setText(R.string.logout) - holder.id = BUTTON_ID_LOGOUT - holder.color = R.color.colorWarning - } - } catch (e: Exception) { - Log.e(TAG, "Unable to add entities to list", e) - } - } - - override fun getItemCount() = max(scenes.size + scripts.size + lights.size + 5, 7) - - override fun getItemViewType(position: Int): Int { - /* - Current layout contains of three sections: - # Scenes - - scene 1 - - scene 2 - - etc - # Scripts - - script 1 - - script 2 - - etc - # Other - - Logout - - Envisioned final layout: - # Scenes - - 3 favorite scenes - - More scenes button - # Devices - - 3 favorite devices - - More devices button - # Scripts - - 3 favorite scripts - - More scripts button - # Other - - Settings - */ - - return when { - position == 0 || position == scenes.size + 1 || position == scenes.size + scripts.size + 2 || position == itemCount - 2 -> TYPE_HEADER - position == itemCount - 1 -> TYPE_BUTTON - position < scenes.size + 1 && scenes.size > 0 -> TYPE_SCENE - position > scenes.size + 1 && scripts.size > 0 -> TYPE_SCENE - else -> TYPE_LOADING - } - } -} diff --git a/wear/src/main/java/io/homeassistant/companion/android/viewHolders/ButtonViewHolder.kt b/wear/src/main/java/io/homeassistant/companion/android/viewHolders/ButtonViewHolder.kt deleted file mode 100644 index 33c58dd8a5c..00000000000 --- a/wear/src/main/java/io/homeassistant/companion/android/viewHolders/ButtonViewHolder.kt +++ /dev/null @@ -1,27 +0,0 @@ -package io.homeassistant.companion.android.viewHolders - -import android.view.View -import android.widget.TextView -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.RecyclerView -import io.homeassistant.companion.android.R - -class ButtonViewHolder(val v: View, val onClick: (String) -> Unit) : - RecyclerView.ViewHolder(v) { - - var txtName: TextView = v.findViewById(R.id.txt_name) - - var color: Int? = null - set(value) { - v.background.mutate().setTint(ContextCompat.getColor(v.context, value!!)) - field = value - } - - var id: String = "" - - init { - v.setOnClickListener { - onClick(id) - } - } -} diff --git a/wear/src/main/java/io/homeassistant/companion/android/viewHolders/EntityButtonViewHolder.kt b/wear/src/main/java/io/homeassistant/companion/android/viewHolders/EntityButtonViewHolder.kt deleted file mode 100644 index 098f60cd142..00000000000 --- a/wear/src/main/java/io/homeassistant/companion/android/viewHolders/EntityButtonViewHolder.kt +++ /dev/null @@ -1,58 +0,0 @@ -package io.homeassistant.companion.android.viewHolders - -import android.graphics.Color -import android.view.View -import android.widget.ImageView -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import com.mikepenz.iconics.IconicsDrawable -import com.mikepenz.iconics.utils.colorInt -import com.mikepenz.iconics.utils.sizeDp -import io.homeassistant.companion.android.R -import io.homeassistant.companion.android.common.data.integration.Entity - -class EntityButtonViewHolder(v: View, val onClick: (Entity<Any>) -> Unit) : - RecyclerView.ViewHolder(v) { - - private var txtName: TextView = v.findViewById(R.id.txt_name) - private var imgIcon: ImageView = v.findViewById(R.id.img_icon) - - var entity: Entity<Any>? = null - set(value) { - val entityAttributes = value?.attributes as Map<String, String> - txtName.text = entityAttributes["friendly_name"] - - if (entityAttributes["icon"]?.startsWith("mdi") == true) { - val icon: String = entityAttributes["icon"]!!.split(":")[1] - val iconDrawable = IconicsDrawable(imgIcon.context, "cmd-$icon").apply { - colorInt = Color.WHITE - sizeDp = 24 - } - imgIcon.setImageDrawable(iconDrawable) - } else { - // Set default icon - when { - value.entityId.split(".")[0] == "script" -> { - imgIcon.setImageResource(R.drawable.ic_scripts) - } - value.entityId.split(".")[0] == "light" -> { - imgIcon.setImageResource(R.drawable.ic_light) - } - value.entityId.split(".")[0] == "cover" -> { - imgIcon.setImageResource(R.drawable.ic_garage) - } - else -> { - imgIcon.setImageResource(R.drawable.ic_scenes) - } - } - } - - field = value - } - - init { - v.setOnClickListener { - entity?.let { onClick(it) } - } - } -} diff --git a/wear/src/main/res/layout/activity_home.xml b/wear/src/main/res/layout/activity_home.xml deleted file mode 100644 index fc005e51daa..00000000000 --- a/wear/src/main/res/layout/activity_home.xml +++ /dev/null @@ -1,12 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<androidx.wear.widget.WearableRecyclerView xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:id="@+id/home_list" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:scrollbars="vertical" - android:paddingBottom="48dp" - android:clipToPadding="false" - tools:listitem="@layout/listitem_entity_button"> - <requestFocus /> -</androidx.wear.widget.WearableRecyclerView> From 2aff414b5cd716ff9804fd7d718f3476b92a4cf3 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi <dshokouhi@gmail.com> Date: Fri, 29 Oct 2021 07:49:41 -0700 Subject: [PATCH 07/11] Remove more unused files --- wear/src/main/res/drawable/ic_garage.xml | 8 ---- wear/src/main/res/drawable/ic_light.xml | 8 ---- wear/src/main/res/drawable/ic_scenes.xml | 10 ----- wear/src/main/res/drawable/ic_scripts.xml | 8 ---- wear/src/main/res/layout/listitem_button.xml | 24 ------------ .../res/layout/listitem_entity_button.xml | 37 ------------------- 6 files changed, 95 deletions(-) delete mode 100644 wear/src/main/res/drawable/ic_garage.xml delete mode 100644 wear/src/main/res/drawable/ic_light.xml delete mode 100644 wear/src/main/res/drawable/ic_scenes.xml delete mode 100644 wear/src/main/res/drawable/ic_scripts.xml delete mode 100644 wear/src/main/res/layout/listitem_button.xml delete mode 100644 wear/src/main/res/layout/listitem_entity_button.xml diff --git a/wear/src/main/res/drawable/ic_garage.xml b/wear/src/main/res/drawable/ic_garage.xml deleted file mode 100644 index 338bcdc2e2e..00000000000 --- a/wear/src/main/res/drawable/ic_garage.xml +++ /dev/null @@ -1,8 +0,0 @@ -<!-- drawable/garage.xml --> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:height="24dp" - android:width="24dp" - android:viewportWidth="24" - android:viewportHeight="24"> - <path android:fillColor="#000" android:pathData="M19,20H17V11H7V20H5V9L12,5L19,9V20M8,12H16V14H8V12M8,15H16V17H8V15M16,18V20H8V18H16Z" /> -</vector> \ No newline at end of file diff --git a/wear/src/main/res/drawable/ic_light.xml b/wear/src/main/res/drawable/ic_light.xml deleted file mode 100644 index 9163645e35d..00000000000 --- a/wear/src/main/res/drawable/ic_light.xml +++ /dev/null @@ -1,8 +0,0 @@ -<!-- drawable/lightbulb.xml --> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:height="24dp" - android:width="24dp" - android:viewportWidth="24" - android:viewportHeight="24"> - <path android:fillColor="#000" android:pathData="M12,2A7,7 0 0,0 5,9C5,11.38 6.19,13.47 8,14.74V17A1,1 0 0,0 9,18H15A1,1 0 0,0 16,17V14.74C17.81,13.47 19,11.38 19,9A7,7 0 0,0 12,2M9,21A1,1 0 0,0 10,22H14A1,1 0 0,0 15,21V20H9V21Z" /> -</vector> \ No newline at end of file diff --git a/wear/src/main/res/drawable/ic_scenes.xml b/wear/src/main/res/drawable/ic_scenes.xml deleted file mode 100644 index 4bf15508591..00000000000 --- a/wear/src/main/res/drawable/ic_scenes.xml +++ /dev/null @@ -1,10 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> - <path - android:fillColor="@android:color/white" - android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9c0.83,0 1.5,-0.67 1.5,-1.5 0,-0.39 -0.15,-0.74 -0.39,-1.01 -0.23,-0.26 -0.38,-0.61 -0.38,-0.99 0,-0.83 0.67,-1.5 1.5,-1.5L16,16c2.76,0 5,-2.24 5,-5 0,-4.42 -4.03,-8 -9,-8zM6.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,9 6.5,9 8,9.67 8,10.5 7.33,12 6.5,12zM9.5,8C8.67,8 8,7.33 8,6.5S8.67,5 9.5,5s1.5,0.67 1.5,1.5S10.33,8 9.5,8zM14.5,8c-0.83,0 -1.5,-0.67 -1.5,-1.5S13.67,5 14.5,5s1.5,0.67 1.5,1.5S15.33,8 14.5,8zM17.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S16.67,9 17.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/> -</vector> diff --git a/wear/src/main/res/drawable/ic_scripts.xml b/wear/src/main/res/drawable/ic_scripts.xml deleted file mode 100644 index 8e2a94bdb43..00000000000 --- a/wear/src/main/res/drawable/ic_scripts.xml +++ /dev/null @@ -1,8 +0,0 @@ -<!-- drawable/script_text.xml --> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:height="24dp" - android:width="24dp" - android:viewportWidth="24" - android:viewportHeight="24"> - <path android:fillColor="#000" android:pathData="M17.8,20C17.4,21.2 16.3,22 15,22H5C3.3,22 2,20.7 2,19V18H5L14.2,18C14.6,19.2 15.7,20 17,20H17.8M19,2C20.7,2 22,3.3 22,5V6H20V5C20,4.4 19.6,4 19,4C18.4,4 18,4.4 18,5V18H17C16.4,18 16,17.6 16,17V16H5V5C5,3.3 6.3,2 8,2H19M8,6V8H15V6H8M8,10V12H14V10H8Z" /> -</vector> \ No newline at end of file diff --git a/wear/src/main/res/layout/listitem_button.xml b/wear/src/main/res/layout/listitem_button.xml deleted file mode 100644 index 494aa9aa180..00000000000 --- a/wear/src/main/res/layout/listitem_button.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="48dp" - android:layout_marginStart="8dp" - android:layout_marginTop="2dp" - android:layout_marginEnd="8dp" - android:layout_marginBottom="2dp" - android:background="@drawable/item_background" - android:orientation="vertical" - android:padding="12dp" - android:backgroundTintMode="src_over"> - - <androidx.appcompat.widget.AppCompatTextView - android:id="@+id/txt_name" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_centerInParent="true" - android:ellipsize="marquee" - android:maxLines="1" - tools:text="@string/other" /> - -</RelativeLayout> \ No newline at end of file diff --git a/wear/src/main/res/layout/listitem_entity_button.xml b/wear/src/main/res/layout/listitem_entity_button.xml deleted file mode 100644 index c2f70fbfc75..00000000000 --- a/wear/src/main/res/layout/listitem_entity_button.xml +++ /dev/null @@ -1,37 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="48dp" - android:layout_marginStart="8dp" - android:layout_marginTop="2dp" - android:layout_marginEnd="8dp" - android:layout_marginBottom="2dp" - android:background="@drawable/item_background" - android:orientation="vertical" - android:padding="12dp"> - - <ImageView - android:id="@+id/img_icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignParentStart="true" - android:layout_alignParentTop="true" - android:layout_alignParentBottom="true" - android:layout_marginEnd="8dp" - app:tint="@color/white" - app:srcCompat="@drawable/ic_scenes" /> - - <androidx.appcompat.widget.AppCompatTextView - android:id="@+id/txt_name" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignBaseline="@id/img_icon" - android:layout_alignParentTop="true" - android:layout_toEndOf="@+id/img_icon" - android:ellipsize="marquee" - android:maxLines="1" - tools:text="@string/scene" /> - -</RelativeLayout> \ No newline at end of file From 88765df033244f17ee5b8a4ceecd86802deb9f7b Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi <dshokouhi@gmail.com> Date: Fri, 29 Oct 2021 14:29:00 -0700 Subject: [PATCH 08/11] Pass entities into composable and use rememberSaveable for the lists --- .../companion/android/home/HomeActivity.kt | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) 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 78ca06687de..8e8c1886fab 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 @@ -11,6 +11,7 @@ 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.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -67,8 +68,11 @@ class HomeActivity : ComponentActivity(), HomeView { .inject(this) if (presenter.onViewReady()) { + val entities = runBlocking { + presenter.getEntities() + } setContent { - LoadHomePage() + LoadHomePage(entities) } } } @@ -91,11 +95,7 @@ class HomeActivity : ComponentActivity(), HomeView { } @Composable - private fun LoadHomePage() { - - val entities = runBlocking { - presenter.getEntities() - } + private fun LoadHomePage(entities: Array<Entity<Any>>) { val scenes = entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "scene" } @@ -109,7 +109,11 @@ class HomeActivity : ComponentActivity(), HomeView { entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "switch" } val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState() - + val scenesRemembered = rememberSaveable { scenes } + val inputBooleansRemembered = rememberSaveable { inputBooleans } + val lightsRemembered = rememberSaveable { lights } + val scriptsRemembered = rememberSaveable { scripts } + val switchesRemembered = rememberSaveable { switches } MaterialTheme { Scaffold( positionIndicator = { @@ -129,40 +133,40 @@ class HomeActivity : ComponentActivity(), HomeView { horizontalAlignment = Alignment.CenterHorizontally, state = scalingLazyListState ) { - if (inputBooleans.isNotEmpty()) { - items(inputBooleans.size) { index -> + if (inputBooleansRemembered.isNotEmpty()) { + items(inputBooleansRemembered.size) { index -> if (index == 0) SetTitle(R.string.input_booleans) - SetEntityUI(inputBooleans[index], index) + SetEntityUI(inputBooleansRemembered[index], index) } } - if (lights.isNotEmpty()) { - items(lights.size) { index -> + if (lightsRemembered.isNotEmpty()) { + items(lightsRemembered.size) { index -> if (index == 0) SetTitle(R.string.lights) - SetEntityUI(lights[index], index) + SetEntityUI(lightsRemembered[index], index) } } - if (scenes.isNotEmpty()) { - items(scenes.size) { index -> + if (scenesRemembered.isNotEmpty()) { + items(scenesRemembered.size) { index -> if (index == 0) SetTitle(R.string.scenes) - SetEntityUI(scenes[index], index) + SetEntityUI(scenesRemembered[index], index) } } - if (scripts.isNotEmpty()) { - items(scripts.size) { index -> + if (scriptsRemembered.isNotEmpty()) { + items(scriptsRemembered.size) { index -> if (index == 0) SetTitle(R.string.scripts) - SetEntityUI(scripts[index], index) + SetEntityUI(scriptsRemembered[index], index) } } - if (switches.isNotEmpty()) { - items(switches.size) { index -> + if (switchesRemembered.isNotEmpty()) { + items(switchesRemembered.size) { index -> if (index == 0) SetTitle(R.string.switches) - SetEntityUI(switches[index], index) + SetEntityUI(switchesRemembered[index], index) } } From a54b8e65fc094794cd3a71d35ba2aa1cf8917a1d Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi <dshokouhi@gmail.com> Date: Mon, 1 Nov 2021 15:41:59 -0700 Subject: [PATCH 09/11] Move getEntities to ViewModel for recomposition --- .../companion/android/home/HomeActivity.kt | 48 +++++++++---------- .../companion/android/home/HomePresenter.kt | 2 - .../android/home/HomePresenterImpl.kt | 9 ---- .../android/viewModels/EntityViewModel.kt | 41 ++++++++++++++++ .../android/viewModels/ViewModuleComponent.kt | 10 ++++ 5 files changed, 73 insertions(+), 37 deletions(-) create mode 100755 wear/src/main/java/io/homeassistant/companion/android/viewModels/EntityViewModel.kt create mode 100755 wear/src/main/java/io/homeassistant/companion/android/viewModels/ViewModuleComponent.kt 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 8e8c1886fab..58b6632ada6 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 @@ -5,13 +5,13 @@ import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.viewModels import androidx.compose.foundation.layout.Column 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.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -41,7 +41,7 @@ import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor import io.homeassistant.companion.android.common.data.integration.Entity import io.homeassistant.companion.android.onboarding.OnboardingActivity import io.homeassistant.companion.android.onboarding.integration.MobileAppIntegrationActivity -import kotlinx.coroutines.runBlocking +import io.homeassistant.companion.android.viewModels.EntityViewModel import javax.inject.Inject class HomeActivity : ComponentActivity(), HomeView { @@ -49,6 +49,8 @@ class HomeActivity : ComponentActivity(), HomeView { @Inject lateinit var presenter: HomePresenter + val entityViewModel by viewModels<EntityViewModel>() + companion object { private const val TAG = "HomeActivity" @@ -68,11 +70,9 @@ class HomeActivity : ComponentActivity(), HomeView { .inject(this) if (presenter.onViewReady()) { - val entities = runBlocking { - presenter.getEntities() - } setContent { - LoadHomePage(entities) + LoadHomePage(entities = entityViewModel.entitiesResponse) + entityViewModel.getEntities(applicationContext) } } } @@ -109,11 +109,7 @@ class HomeActivity : ComponentActivity(), HomeView { entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "switch" } val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState() - val scenesRemembered = rememberSaveable { scenes } - val inputBooleansRemembered = rememberSaveable { inputBooleans } - val lightsRemembered = rememberSaveable { lights } - val scriptsRemembered = rememberSaveable { scripts } - val switchesRemembered = rememberSaveable { switches } + MaterialTheme { Scaffold( positionIndicator = { @@ -133,40 +129,40 @@ class HomeActivity : ComponentActivity(), HomeView { horizontalAlignment = Alignment.CenterHorizontally, state = scalingLazyListState ) { - if (inputBooleansRemembered.isNotEmpty()) { - items(inputBooleansRemembered.size) { index -> + if (inputBooleans.isNotEmpty()) { + items(inputBooleans.size) { index -> if (index == 0) SetTitle(R.string.input_booleans) - SetEntityUI(inputBooleansRemembered[index], index) + SetEntityUI(inputBooleans[index], index) } } - if (lightsRemembered.isNotEmpty()) { - items(lightsRemembered.size) { index -> + if (lights.isNotEmpty()) { + items(lights.size) { index -> if (index == 0) SetTitle(R.string.lights) - SetEntityUI(lightsRemembered[index], index) + SetEntityUI(lights[index], index) } } - if (scenesRemembered.isNotEmpty()) { - items(scenesRemembered.size) { index -> + if (scenes.isNotEmpty()) { + items(scenes.size) { index -> if (index == 0) SetTitle(R.string.scenes) - SetEntityUI(scenesRemembered[index], index) + SetEntityUI(scenes[index], index) } } - if (scriptsRemembered.isNotEmpty()) { - items(scriptsRemembered.size) { index -> + if (scripts.isNotEmpty()) { + items(scripts.size) { index -> if (index == 0) SetTitle(R.string.scripts) - SetEntityUI(scriptsRemembered[index], index) + SetEntityUI(scripts[index], index) } } - if (switchesRemembered.isNotEmpty()) { - items(switchesRemembered.size) { index -> + if (switches.isNotEmpty()) { + items(switches.size) { index -> if (index == 0) SetTitle(R.string.switches) - SetEntityUI(switchesRemembered[index], index) + SetEntityUI(switches[index], index) } } 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 25d748f45b4..7e89709d796 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 @@ -8,6 +8,4 @@ interface HomePresenter { fun onEntityClicked(entity: Entity<Any>) fun onLogoutClicked() fun onFinish() - - suspend fun getEntities(): Array<Entity<Any>> } 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 87cdc3fcf9f..7f85beb7490 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 @@ -47,15 +47,6 @@ class HomePresenterImpl @Inject constructor( return isRegistered } - override suspend fun getEntities(): Array<Entity<Any>> { - return try { - integrationUseCase.getEntities() - } catch (e: Exception) { - Log.e(TAG, "Unable to get entities", e) - emptyArray() - } - } - override fun onEntityClicked(entity: Entity<Any>) { if (entity.entityId.split(".")[0] in toggleDomains) { diff --git a/wear/src/main/java/io/homeassistant/companion/android/viewModels/EntityViewModel.kt b/wear/src/main/java/io/homeassistant/companion/android/viewModels/EntityViewModel.kt new file mode 100755 index 00000000000..d703d9b2582 --- /dev/null +++ b/wear/src/main/java/io/homeassistant/companion/android/viewModels/EntityViewModel.kt @@ -0,0 +1,41 @@ +package io.homeassistant.companion.android.viewModels + +import android.content.Context +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor +import io.homeassistant.companion.android.common.data.integration.Entity +import io.homeassistant.companion.android.common.data.integration.IntegrationRepository +import kotlinx.coroutines.launch +import javax.inject.Inject + +class EntityViewModel : ViewModel() { + + @Inject + lateinit var integrationUseCase: IntegrationRepository + + var entitiesResponse: Array<Entity<Any>> by mutableStateOf(arrayOf()) + + companion object { + private const val TAG = "EntityViewModel" + } + + fun getEntities(context: Context) { + DaggerViewModuleComponent.builder() + .appComponent((context as GraphComponentAccessor).appComponent) + .build() + .inject(this) + viewModelScope.launch { + try { + val entities = integrationUseCase.getEntities() + entitiesResponse = entities + } catch (e: Exception) { + Log.e(TAG, "Unable to get list of entities", e) + } + } + } +} diff --git a/wear/src/main/java/io/homeassistant/companion/android/viewModels/ViewModuleComponent.kt b/wear/src/main/java/io/homeassistant/companion/android/viewModels/ViewModuleComponent.kt new file mode 100755 index 00000000000..01255428438 --- /dev/null +++ b/wear/src/main/java/io/homeassistant/companion/android/viewModels/ViewModuleComponent.kt @@ -0,0 +1,10 @@ +package io.homeassistant.companion.android.viewModels + +import dagger.Component +import io.homeassistant.companion.android.common.dagger.AppComponent + +@Component(dependencies = [AppComponent::class]) +interface ViewModuleComponent { + + fun inject(entityViewModel: EntityViewModel) +} From 3d70346460c54e20a4d46f077e3e129247e8702e Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi <dshokouhi@gmail.com> Date: Mon, 1 Nov 2021 15:52:09 -0700 Subject: [PATCH 10/11] Restore onViewReady changes --- .../homeassistant/companion/android/home/HomeActivity.kt | 9 ++++----- .../companion/android/home/HomePresenter.kt | 2 +- .../companion/android/home/HomePresenterImpl.kt | 8 ++------ 3 files changed, 7 insertions(+), 12 deletions(-) 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 58b6632ada6..019bf2ef13c 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 @@ -69,11 +69,10 @@ class HomeActivity : ComponentActivity(), HomeView { .build() .inject(this) - if (presenter.onViewReady()) { - setContent { - LoadHomePage(entities = entityViewModel.entitiesResponse) - entityViewModel.getEntities(applicationContext) - } + presenter.onViewReady() + setContent { + LoadHomePage(entities = entityViewModel.entitiesResponse) + entityViewModel.getEntities(applicationContext) } } 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 7e89709d796..8cf1a229e9a 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 @@ -4,7 +4,7 @@ import io.homeassistant.companion.android.common.data.integration.Entity interface HomePresenter { - fun onViewReady(): Boolean + fun onViewReady() fun onEntityClicked(entity: Entity<Any>) fun onLogoutClicked() fun onFinish() 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 7f85beb7490..fb3c69cc007 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 @@ -12,7 +12,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import javax.inject.Inject class HomePresenterImpl @Inject constructor( @@ -31,20 +30,17 @@ class HomePresenterImpl @Inject constructor( private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job()) - override fun onViewReady(): Boolean { - var isRegistered = false - runBlocking { + override fun onViewReady() { + mainScope.launch { val sessionValid = authenticationUseCase.getSessionState() == SessionState.CONNECTED if (sessionValid && integrationUseCase.isRegistered()) { resyncRegistration() - isRegistered = true } else if (sessionValid) { view.displayMobileAppIntegration() } else { view.displayOnBoarding() } } - return isRegistered } override fun onEntityClicked(entity: Entity<Any>) { From a83ba1212c0298e4e7b51a33bb1aea516d79c540 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi <dshokouhi@gmail.com> Date: Mon, 1 Nov 2021 21:24:23 -0700 Subject: [PATCH 11/11] Add a loading screen while waiting on viewmodel --- .../companion/android/home/HomeActivity.kt | 195 ++++++++++-------- wear/src/main/res/values/strings.xml | 1 + 2 files changed, 115 insertions(+), 81 deletions(-) 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 019bf2ef13c..1c42b6523b8 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 @@ -8,6 +8,7 @@ import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -23,6 +24,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.wear.compose.material.Chip +import androidx.wear.compose.material.ChipColors import androidx.wear.compose.material.ChipDefaults import androidx.wear.compose.material.MaterialTheme import androidx.wear.compose.material.PositionIndicator @@ -49,7 +51,7 @@ class HomeActivity : ComponentActivity(), HomeView { @Inject lateinit var presenter: HomePresenter - val entityViewModel by viewModels<EntityViewModel>() + private val entityViewModel by viewModels<EntityViewModel>() companion object { private const val TAG = "HomeActivity" @@ -96,96 +98,119 @@ class HomeActivity : ComponentActivity(), HomeView { @Composable private fun LoadHomePage(entities: Array<Entity<Any>>) { - val scenes = - entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "scene" } - val scripts = - entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "script" } - val lights = - entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "light" } - val inputBooleans = entities.sortedBy { it.entityId } - .filter { it.entityId.split(".")[0] == "input_boolean" } - val switches = - entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "switch" } - - val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState() - - MaterialTheme { - Scaffold( - positionIndicator = { - if (scalingLazyListState.isScrollInProgress) - PositionIndicator(scalingLazyListState = scalingLazyListState) - } - ) { - ScalingLazyColumn( + if (entities.isNullOrEmpty()) { + Column { + Spacer( + modifier = Modifier + .fillMaxWidth() + .padding(top = 10.dp) + ) + SetTitle(id = R.string.loading) + Chip( modifier = Modifier - .fillMaxSize(), - contentPadding = PaddingValues( - top = 10.dp, - start = 10.dp, - end = 10.dp, - bottom = 40.dp - ), - horizontalAlignment = Alignment.CenterHorizontally, - state = scalingLazyListState + .padding(top = 50.dp, start = 10.dp, end = 10.dp), + label = { + Text( + text = stringResource(R.string.loading_entities), + textAlign = TextAlign.Center + ) + }, + onClick = { /* No op */ }, + colors = setChipDefaults() + ) + } + } else { + val scenes = + entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "scene" } + val scripts = + entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "script" } + val lights = + entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "light" } + val inputBooleans = entities.sortedBy { it.entityId } + .filter { it.entityId.split(".")[0] == "input_boolean" } + val switches = + entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "switch" } + + val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState() + + MaterialTheme { + Scaffold( + positionIndicator = { + if (scalingLazyListState.isScrollInProgress) + PositionIndicator(scalingLazyListState = scalingLazyListState) + } ) { - if (inputBooleans.isNotEmpty()) { - items(inputBooleans.size) { index -> - if (index == 0) - SetTitle(R.string.input_booleans) - SetEntityUI(inputBooleans[index], index) + ScalingLazyColumn( + modifier = Modifier + .fillMaxSize(), + contentPadding = PaddingValues( + top = 10.dp, + start = 10.dp, + end = 10.dp, + bottom = 40.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + state = scalingLazyListState + ) { + if (inputBooleans.isNotEmpty()) { + items(inputBooleans.size) { index -> + if (index == 0) + SetTitle(R.string.input_booleans) + SetEntityUI(inputBooleans[index], index) + } } - } - if (lights.isNotEmpty()) { - items(lights.size) { index -> - if (index == 0) - SetTitle(R.string.lights) - SetEntityUI(lights[index], index) + if (lights.isNotEmpty()) { + items(lights.size) { index -> + if (index == 0) + SetTitle(R.string.lights) + SetEntityUI(lights[index], index) + } } - } - if (scenes.isNotEmpty()) { - items(scenes.size) { index -> - if (index == 0) - SetTitle(R.string.scenes) + if (scenes.isNotEmpty()) { + items(scenes.size) { index -> + if (index == 0) + SetTitle(R.string.scenes) - SetEntityUI(scenes[index], index) + SetEntityUI(scenes[index], index) + } } - } - if (scripts.isNotEmpty()) { - items(scripts.size) { index -> - if (index == 0) - SetTitle(R.string.scripts) - SetEntityUI(scripts[index], index) + if (scripts.isNotEmpty()) { + items(scripts.size) { index -> + if (index == 0) + SetTitle(R.string.scripts) + SetEntityUI(scripts[index], index) + } } - } - if (switches.isNotEmpty()) { - items(switches.size) { index -> - if (index == 0) - SetTitle(R.string.switches) - SetEntityUI(switches[index], index) + if (switches.isNotEmpty()) { + items(switches.size) { index -> + if (index == 0) + SetTitle(R.string.switches) + SetEntityUI(switches[index], index) + } } - } - item { - Column { - SetTitle(R.string.other) - Chip( - modifier = Modifier - .fillMaxWidth() - .padding(top = 10.dp), - icon = { - Image(asset = CommunityMaterial.Icon.cmd_exit_run) - }, - label = { - Text( - text = stringResource(id = R.string.logout) + item { + Column { + SetTitle(R.string.other) + Chip( + modifier = Modifier + .fillMaxWidth() + .padding(top = 10.dp), + icon = { + Image(asset = CommunityMaterial.Icon.cmd_exit_run) + }, + label = { + Text( + text = stringResource(id = R.string.logout) + ) + }, + onClick = { presenter.onLogoutClicked() }, + colors = ChipDefaults.primaryChipColors( + backgroundColor = Color.Red, + contentColor = Color.Black ) - }, - onClick = { presenter.onLogoutClicked() }, - colors = ChipDefaults.primaryChipColors( - backgroundColor = Color.Red, - contentColor = Color.Black ) - ) + } } } } @@ -228,7 +253,7 @@ class HomeActivity : ComponentActivity(), HomeView { ) }, onClick = { presenter.onEntityClicked(entity) }, - colors = ChipDefaults.primaryChipColors(backgroundColor = colorResource(id = R.color.colorAccent), contentColor = Color.Black) + colors = setChipDefaults() ) } @@ -243,4 +268,12 @@ class HomeActivity : ComponentActivity(), HomeView { .fillMaxWidth() ) } + + @Composable + private fun setChipDefaults(): ChipColors { + return ChipDefaults.primaryChipColors( + backgroundColor = colorResource(id = R.color.colorAccent), + contentColor = Color.Black + ) + } } diff --git a/wear/src/main/res/values/strings.xml b/wear/src/main/res/values/strings.xml index 57bad2b4db1..d94b729a521 100644 --- a/wear/src/main/res/values/strings.xml +++ b/wear/src/main/res/values/strings.xml @@ -33,4 +33,5 @@ <string name="lights">Lights</string> <string name="input_booleans">Input Booleans</string> <string name="switches">Switches</string> + <string name="loading_entities">Please wait while we load your entities</string> </resources> \ No newline at end of file