Skip to content

Commit

Permalink
Convert InspectFragment to Jetpack Compose. (#189)
Browse files Browse the repository at this point in the history
See https://developer.android.com/codelabs/jetpack-compose-migration#0 for background information.
Still in Step 1: very simple migration as a first step. bottom-up approach where we simply migrate the Inspect screen view subtree that is shown when a device is being inspected.

Minor fix to ClustersMap.
  • Loading branch information
pierredelisle authored Jan 24, 2024
1 parent 6ae3fdb commit c056650
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 99 deletions.
1 change: 1 addition & 0 deletions 3p-ecosystem/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ dependencies {
// themselves. When you update the BOM version, all the libraries that you're using are
// automatically updated to their new versions.
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.lifecycle.runtime.compose)
androidTestImplementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.foundation.layout)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ object MatterConstants {
42L to "OTA Software Update Requestor",
43L to "Localization Configuration",
44L to "Time Format Localization",
44L to "Time Format Localization",
44L to "Unit Localization",
45L to "Unit Localization",
48L to "General Commissioning",
49L to "Network Commissioning",
50L to "Diagnostics Logs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,11 @@ class SubscriptionHelper @Inject constructor(private val chipClient: ChipClient)
}

override fun onReport(nodeState: NodeState) {
Timber.d("reportCallback: onReport")
//Timber.d("reportCallback: onReport")
val debugString = nodeStateToDebugString(nodeState)
Timber.d("------- BEGIN REPORT -----")
Timber.d(debugString)
Timber.d("------- END REPORT -----")
//Timber.d("------- BEGIN REPORT -----")
//Timber.d(debugString)
//Timber.d("------- END REPORT -----")
}

override fun onDone() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,32 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.tooling.preview.Preview
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.navigation.fragment.findNavController
import com.google.homesampleapp.Device
import com.google.homesampleapp.R
import com.google.homesampleapp.chip.DeviceMatterInfo
import com.google.homesampleapp.chip.MatterConstants
import com.google.homesampleapp.data.DevicesStateRepository
import com.google.homesampleapp.databinding.FragmentInspectBinding
import com.google.homesampleapp.lifeCycleEvent
import com.google.homesampleapp.screens.shared.SelectedDeviceViewModel
import com.google.protobuf.Timestamp
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import timber.log.Timber
import javax.inject.Inject

/**
* The Inspect Fragment shows all the "cluster" information about the device that was selected in
Expand Down Expand Up @@ -67,7 +77,22 @@ class InspectFragment : Fragment() {
Timber.d(lifeCycleEvent("onCreateView()"))

// Setup the binding with the fragment.
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_inspect, container, false)
binding = DataBindingUtil.inflate<FragmentInspectBinding>(
inflater,
R.layout.fragment_inspect,
container,
false
).apply {
composeView.apply {
// Dispose the Composition when the view's LifecycleOwner is destroyed
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MaterialTheme {
InspectRoute()
}
}
}
}

// Setup UI elements and livedata observers.
setupUiElements()
Expand All @@ -76,6 +101,11 @@ class InspectFragment : Fragment() {
return binding.root
}

override fun onResume() {
Timber.d("onResume")
super.onResume()
}

// -----------------------------------------------------------------------------------------------
// Setup UI elements

Expand All @@ -84,109 +114,158 @@ class InspectFragment : Fragment() {
binding.topAppBar.setOnClickListener { findNavController().popBackStack() }
}

// -----------------------------------------------------------------------------------------------
// Setup Observers

private fun setupObservers() {
// Observer on the currently selected device
selectedDeviceViewModel.selectedDeviceIdLiveData.observe(viewLifecycleOwner) { deviceId ->
Timber.d(
"selectedDeviceViewModel.selectedDeviceIdLiveData.observe is called with deviceId [${deviceId}]")
updateDeviceInfo()
selectedDeviceViewModel.selectedDeviceLiveData.observe(viewLifecycleOwner) {
binding.topAppBar.title = it?.device?.name
}

// Observer on introspection information
viewModel.instrospectionInfo.observe(viewLifecycleOwner) { updateIntrospectionInfo(it) }
}

// -----------------------------------------------------------------------------------------------
// UI update functions
// Composables

private fun updateDeviceInfo() {
Timber.d("updateDeviceIfo")
if (selectedDeviceViewModel.selectedDeviceIdLiveData.value == -1L) {
// Device was just removed, nothing to do. We'll move to HomeFragment.
return
@Composable
private fun InspectRoute() {
// Observes values needed by the InspectScreen.
val selectedDeviceId by selectedDeviceViewModel.selectedDeviceIdLiveData.observeAsState()
val instrospectionInfo by viewModel.instrospectionInfo.observeAsState()

LifecycleResumeEffect {
Timber.d("LifecycleResumeEffect: selectedDeviceId [$selectedDeviceId]")
viewModel.inspectDevice(selectedDeviceViewModel.selectedDeviceLiveData.value!!.device.deviceId)
onPauseOrDispose {
// do any needed clean up here
Timber.d("LifecycleResumeEffect:onPauseOrDispose")
}
}
val deviceUiModel = selectedDeviceViewModel.selectedDeviceLiveData.value
Timber.d(
"updateDeviceIfo with device [${deviceUiModel?.device?.deviceId}] [${deviceUiModel?.device?.name}]")

binding.topAppBar.title = deviceUiModel?.device?.name
// Fetch device introspection info, and then live data will be updated, which is observed
// by the fragment.
viewModel.inspectDevice(deviceUiModel?.device?.deviceId!!)

InspectScreen(selectedDeviceId, instrospectionInfo)
}

private fun updateIntrospectionInfo(deviceMatterInfoList: List<DeviceMatterInfo>) {
val linearLayout = binding.inspectInfoLayout
linearLayout.removeAllViews()
if (deviceMatterInfoList.isEmpty()) {
addTextView(
"Oops... We could not retrieve any information from the Descriptor Cluster. " +
"This is probably because the device just recently turned \"offline\".",
com.google.android.material.R.style.TextAppearance_Material3_BodyMedium)
@Composable
private fun InspectScreen(
selectedDeviceId: Long?,
deviceMatterInfoList: List<DeviceMatterInfo>?
) {
if (selectedDeviceId == -1L) {
// Device was just removed, nothing to do. We'll move to HomeFragment.
return
}
// Add the Descriptor Cluster Title
addTextView(
"Descriptor Cluster",
com.google.android.material.R.style.TextAppearance_Material3_TitleLarge)
// For each endpoint
for (deviceMatterInfo in deviceMatterInfoList) {
// Endpoint ID
addTextView(
"Endpoint ${deviceMatterInfo.endpoint}",
com.google.android.material.R.style.TextAppearance_Material3_TitleMedium)
// Device Types
addTextView(
"Device Types", com.google.android.material.R.style.TextAppearance_Material3_TitleSmall)
for (deviceType in deviceMatterInfo.types) {
val hex = String.format("0x%04X", deviceType)
val typeString = MatterConstants.DeviceTypesMap.getOrDefault(deviceType, "Unknown")
addTextView(
"[${hex}] ${typeString}",
com.google.android.material.R.style.TextAppearance_Material3_BodyMedium)
Column (
// FIXME: Triggers java.lang.IllegalStateException: Vertically scrollable component
// was measured with an infinity maximum height constraints, which is disallowed.
// modifier = Modifier.verticalScroll(rememberScrollState())
) {
if (deviceMatterInfoList == null) {
// The viewModel.inspectDevice() call has not yet completed.
return
}
if (deviceMatterInfoList.isEmpty()) {
Text(
text = "Oops... We could not retrieve any information from the Descriptor Cluster. " +
"This is probably because the device just recently turned \"offline\".",
style = MaterialTheme.typography.bodyMedium)
return
}
// Server Clusters
addTextView(
"Server Clusters",
com.google.android.material.R.style.TextAppearance_Material3_TitleSmall)
if (deviceMatterInfo.serverClusters.isEmpty()) {
addTextView("None", com.google.android.material.R.style.TextAppearance_Material3_BodyMedium)
} else {
for (serverCluster in deviceMatterInfo.serverClusters) {
val hex = String.format("0x%04X", serverCluster)
val serverClusterString =
// Add the Descriptor Cluster Title
Text(
text = "Descriptor Cluster",
style = MaterialTheme.typography.titleLarge)
// For each endpoint
for (deviceMatterInfo in deviceMatterInfoList) {
// Endpoint ID
Text(
text = "<<< Endpoint ${deviceMatterInfo.endpoint} >>>",
style = MaterialTheme.typography.titleMedium)
// Device Types
Text(text = "Device Types", style = MaterialTheme.typography.titleSmall)
for (deviceType in deviceMatterInfo.types) {
val hex = String.format("0x%04X", deviceType)
val typeString = MatterConstants.DeviceTypesMap.getOrDefault(deviceType, "Unknown")
Text(
text = "[${hex}] $typeString",
style = MaterialTheme.typography.bodySmall)
}
// Server Clusters
Text(
text = "Server Clusters",
style = MaterialTheme.typography.titleSmall)
if (deviceMatterInfo.serverClusters.isEmpty()) {
Text(text = "None", style = MaterialTheme.typography.bodySmall)
} else {
for (serverCluster in deviceMatterInfo.serverClusters) {
val hex = String.format("0x%04X", serverCluster)
val serverClusterString =
MatterConstants.ClustersMap.getOrDefault(serverCluster, "Unknown")
addTextView(
"[${hex}] ${serverClusterString}",
com.google.android.material.R.style.TextAppearance_Material3_BodyMedium)
Text(
text = "[${hex}] $serverClusterString",
style = MaterialTheme.typography.bodySmall)
}
}
}
// Client Clusters
addTextView(
"Client Clusters",
com.google.android.material.R.style.TextAppearance_Material3_TitleSmall)
if (deviceMatterInfo.clientClusters.isEmpty()) {
addTextView("None", com.google.android.material.R.style.TextAppearance_Material3_BodyMedium)
} else {
for (clientCluster in deviceMatterInfo.clientClusters) {
val hex = String.format("0x%04X", clientCluster)
val clientClusterString =
// Client Clusters
Text(
text = "Client Clusters",
style = MaterialTheme.typography.titleSmall)
if (deviceMatterInfo.clientClusters.isEmpty()) {
Text(text = "None", style = MaterialTheme.typography.bodySmall)
} else {
for (clientCluster in deviceMatterInfo.clientClusters) {
val hex = String.format("0x%04X", clientCluster)
val clientClusterString =
MatterConstants.ClustersMap.getOrDefault(clientCluster, "Unknown")
addTextView(
"[${hex}] ${clientClusterString}",
com.google.android.material.R.style.TextAppearance_Material3_BodyMedium)
Text(
text = "[${hex}] $clientClusterString",
style = MaterialTheme.typography.bodySmall)
}
}
}
}
}

private fun addTextView(text: String, style: Int) {
val textView = TextView(requireContext())
textView.text = text
textView.setTextAppearance(style)
binding.inspectInfoLayout.addView(textView)
// -----------------------------------------------------------------------------------------------
// Composable Previews

@Preview(widthDp = 300)
@Composable
private fun InspectScreenOfflinePreview() {
MaterialTheme {
InspectScreen(1L, emptyList())
}
}

@Preview(widthDp = 300)
@Composable
private fun InspectScreenOnlineNoClustersPreview() {
MaterialTheme {
InspectScreen(1L,
listOf(
DeviceMatterInfo(1, listOf(15L, 22L), emptyList(), emptyList())
))
}
}

@Preview(widthDp = 300)
@Composable
private fun InspectScreenOnlineWithClustersPreview() {
MaterialTheme {
InspectScreen(1L,
listOf(
DeviceMatterInfo(0, listOf(15L, 22L),
listOf(3L),
listOf(43L, 48L)),
DeviceMatterInfo(1, listOf(15L, 22L),
listOf(3L, 4L, 5L),
listOf(43L, 44L, 45L, 48L))
))
}
}

private val DeviceTest = Device.newBuilder()
.setDeviceId(1L)
.setDeviceType(Device.DeviceType.TYPE_OUTLET)
.setDateCommissioned(Timestamp.getDefaultInstance())
.setName("MyOutlet")
.setProductId("8785")
.setVendorId("6006")
.setRoom("Office")
.build()
}
9 changes: 3 additions & 6 deletions 3p-ecosystem/src/main/res/layout/fragment_inspect.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,10 @@
android:orientation="vertical"
android:layout_margin="16dp"
>
<LinearLayout
android:id="@+id/inspectInfoLayout"
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
</LinearLayout>
android:layout_height="match_parent"/>
</LinearLayout>

</androidx.core.widget.NestedScrollView>
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ runner = "1.5.2"
timber = "5.0.1"
uiautomator = "2.2.0"
zxing = "4.1.0"
lifecycleRuntimeCompose = "2.7.0"

# Dependencies
[libraries]
Expand Down Expand Up @@ -82,6 +83,7 @@ runner = { module = "androidx.test:runner", version.ref = "runner" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "uiautomator" }
zxing = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxing"}
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" }

# Plugins
[plugins]
Expand Down

0 comments on commit c056650

Please sign in to comment.