diff --git a/3p-ecosystem/build.gradle.kts b/3p-ecosystem/build.gradle.kts index df729ec..d87f44a 100644 --- a/3p-ecosystem/build.gradle.kts +++ b/3p-ecosystem/build.gradle.kts @@ -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) diff --git a/3p-ecosystem/src/main/java/com/google/homesampleapp/chip/MatterConstants.kt b/3p-ecosystem/src/main/java/com/google/homesampleapp/chip/MatterConstants.kt index ade35b2..ae3e4e8 100644 --- a/3p-ecosystem/src/main/java/com/google/homesampleapp/chip/MatterConstants.kt +++ b/3p-ecosystem/src/main/java/com/google/homesampleapp/chip/MatterConstants.kt @@ -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", diff --git a/3p-ecosystem/src/main/java/com/google/homesampleapp/chip/SubscriptionHelper.kt b/3p-ecosystem/src/main/java/com/google/homesampleapp/chip/SubscriptionHelper.kt index 815f0b4..5d41983 100644 --- a/3p-ecosystem/src/main/java/com/google/homesampleapp/chip/SubscriptionHelper.kt +++ b/3p-ecosystem/src/main/java/com/google/homesampleapp/chip/SubscriptionHelper.kt @@ -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() { diff --git a/3p-ecosystem/src/main/java/com/google/homesampleapp/screens/inspect/InspectFragment.kt b/3p-ecosystem/src/main/java/com/google/homesampleapp/screens/inspect/InspectFragment.kt index 75dbe8e..32e5a59 100644 --- a/3p-ecosystem/src/main/java/com/google/homesampleapp/screens/inspect/InspectFragment.kt +++ b/3p-ecosystem/src/main/java/com/google/homesampleapp/screens/inspect/InspectFragment.kt @@ -20,12 +20,21 @@ 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 @@ -33,9 +42,10 @@ 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 @@ -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( + 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() @@ -76,6 +101,11 @@ class InspectFragment : Fragment() { return binding.root } + override fun onResume() { + Timber.d("onResume") + super.onResume() + } + // ----------------------------------------------------------------------------------------------- // Setup UI elements @@ -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) { - 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? + ) { + 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() } diff --git a/3p-ecosystem/src/main/res/layout/fragment_inspect.xml b/3p-ecosystem/src/main/res/layout/fragment_inspect.xml index c2e31d0..3e97e5b 100644 --- a/3p-ecosystem/src/main/res/layout/fragment_inspect.xml +++ b/3p-ecosystem/src/main/res/layout/fragment_inspect.xml @@ -58,13 +58,10 @@ android:orientation="vertical" android:layout_margin="16dp" > - - + android:layout_height="match_parent"/> diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9207128..2a26766 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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] @@ -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]