Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert wear home activity to compose #1844

Merged
merged 11 commits into from
Nov 3, 2021
16 changes: 14 additions & 2 deletions wear/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ android {

defaultConfig {
applicationId = "io.homeassistant.companion.android"
minSdk = 23
minSdk = 25
targetSdk = 30

versionName = System.getenv("VERSION") ?: "LOCAL"
Expand All @@ -26,6 +26,11 @@ android {

buildFeatures {
viewBinding = true
compose = true
}

composeOptions {
kotlinCompilerExtensionVersion = "1.0.4"
}

compileOptions {
Expand Down Expand Up @@ -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")

Expand All @@ -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.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")
implementation("androidx.wear.compose:compose-material:1.0.0-alpha09")
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,55 @@ 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.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
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
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
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
import io.homeassistant.companion.android.DaggerPresenterComponent
import io.homeassistant.companion.android.PresenterModule
import io.homeassistant.companion.android.R
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 io.homeassistant.companion.android.viewModels.EntityViewModel
import javax.inject.Inject

class HomeActivity : AppCompatActivity(), HomeView {
class HomeActivity : ComponentActivity(), HomeView {

@Inject
lateinit var presenter: HomePresenter

private lateinit var adapter: HomeListAdapter
private val entityViewModel by viewModels<EntityViewModel>()

companion object {
private const val TAG = "HomeActivity"
Expand All @@ -33,53 +64,25 @@ class HomeActivity : AppCompatActivity(), HomeView {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContentView(R.layout.activity_home)

DaggerPresenterComponent
.builder()
.appComponent((application as GraphComponentAccessor).appComponent)
.presenterModule(PresenterModule(this))
.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 = [email protected]
}

presenter.onViewReady()
setContent {
LoadHomePage(entities = entityViewModel.entitiesResponse)
entityViewModel.getEntities(applicationContext)
}
}

override fun onDestroy() {
presenter.onFinish()
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)
Expand All @@ -91,4 +94,186 @@ class HomeActivity : AppCompatActivity(), HomeView {
startActivity(intent)
finish()
}

@Composable
private fun LoadHomePage(entities: Array<Entity<Any>>) {

if (entities.isNullOrEmpty()) {
Column {
Spacer(
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp)
)
SetTitle(id = R.string.loading)
Chip(
modifier = Modifier
.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)
}
) {
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)
}
}

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
)
)
}
}
}
}
}
}
}

@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
.fillMaxWidth()
.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(),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
},
onClick = { presenter.onEntityClicked(entity) },
colors = setChipDefaults()
)
}

@Composable
private fun SetTitle(id: Int) {
Text(
text = stringResource(id = id),
textAlign = TextAlign.Center,
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier
.fillMaxWidth()
)
}

@Composable
private fun setChipDefaults(): ChipColors {
return ChipDefaults.primaryChipColors(
backgroundColor = colorResource(id = R.color.colorAccent),
contentColor = Color.Black
)
}
}
Loading