diff --git a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/SelectActionFragment.kt b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/SelectActionFragment.kt index fb8141e72d33a8..09c3f85a454963 100644 --- a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/SelectActionFragment.kt +++ b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/SelectActionFragment.kt @@ -76,6 +76,7 @@ class SelectActionFragment : Fragment() { binding.groupSettingBtn.setOnClickListener { handleGroupSettingClicked() } binding.otaProviderBtn.setOnClickListener { handleOTAProviderClicked() } binding.icdBtn.setOnClickListener { handleICDClicked() } + binding.modeSelectBtn.setOnClickListener { handleModeSelectClicked() } return binding.root } @@ -255,6 +256,10 @@ class SelectActionFragment : Fragment() { showFragment(ICDFragment.newInstance()) } + private fun handleModeSelectClicked() { + showFragment(ModeSelectClientFragment.newInstance()) + } + companion object { @JvmStatic fun newInstance() = SelectActionFragment() diff --git a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/ModeSelectClientFragment.kt b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/ModeSelectClientFragment.kt new file mode 100644 index 00000000000000..e19e0fe0a58682 --- /dev/null +++ b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/ModeSelectClientFragment.kt @@ -0,0 +1,297 @@ +package com.google.chip.chiptool.clusterclient + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.EditText +import android.widget.TextView +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import chip.devicecontroller.ChipClusters +import chip.devicecontroller.ChipDeviceController +import chip.devicecontroller.ClusterIDMapping +import chip.devicecontroller.ClusterIDMapping.ModeSelect +import chip.devicecontroller.ReportCallback +import chip.devicecontroller.WriteAttributesCallback +import chip.devicecontroller.cluster.structs.ModeSelectClusterModeOptionStruct +import chip.devicecontroller.model.AttributeState +import chip.devicecontroller.model.AttributeWriteRequest +import chip.devicecontroller.model.ChipAttributePath +import chip.devicecontroller.model.ChipEventPath +import chip.devicecontroller.model.ChipPathId +import chip.devicecontroller.model.NodeState +import chip.devicecontroller.model.Status +import com.google.chip.chiptool.ChipClient +import com.google.chip.chiptool.R +import com.google.chip.chiptool.databinding.ModeSelectFragmentBinding +import java.util.Optional +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import matter.tlv.AnonymousTag +import matter.tlv.TlvReader +import matter.tlv.TlvWriter + +class ModeSelectClientFragment : Fragment() { + private val deviceController: ChipDeviceController + get() = ChipClient.getDeviceController(requireContext()) + + private lateinit var scope: CoroutineScope + + private lateinit var addressUpdateFragment: AddressUpdateFragment + + private var _binding: ModeSelectFragmentBinding? = null + + private val startUpMode: UInt + get() = binding.startUpModeEd.text.toString().toUIntOrNull() ?: 0U + + private val onMode: UInt + get() = binding.onModeEd.text.toString().toUIntOrNull() ?: 0U + + private val currentMode: Int + get() = binding.supportedModesSp.selectedItem.toString().split("-")[0].toInt() + + private val binding + get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = ModeSelectFragmentBinding.inflate(inflater, container, false) + scope = viewLifecycleOwner.lifecycleScope + + addressUpdateFragment = + childFragmentManager.findFragmentById(R.id.addressUpdateFragment) as AddressUpdateFragment + + binding.readAttributeBtn.setOnClickListener { scope.launch { readAttributeBtnClick() } } + binding.changeToModeBtn.setOnClickListener { scope.launch { changeToModeBtnClick() } } + binding.onModeWriteBtn.setOnClickListener { + scope.launch { writeAttributeBtnClick(ClusterIDMapping.ModeSelect.Attribute.OnMode, onMode) } + } + binding.startUpModeWriteBtn.setOnClickListener { + scope.launch { + writeAttributeBtnClick(ClusterIDMapping.ModeSelect.Attribute.StartUpMode, startUpMode) + } + } + + return binding.root + } + + private suspend fun readAttributeBtnClick() { + val endpointId = addressUpdateFragment.endpointId + val clusterId = ModeSelect.ID + val attributeId = ChipPathId.forWildcard().id + val path = ChipAttributePath.newInstance(endpointId, clusterId, attributeId) + val devicePtr = + try { + ChipClient.getConnectedDevicePointer(requireContext(), addressUpdateFragment.deviceId) + } catch (e: IllegalStateException) { + Log.d(TAG, "getConnectedDevicePointer exception", e) + showMessage("Get DevicePointer fail!") + return + } + deviceController.readAttributePath( + object : ReportCallback { + override fun onError( + attributePath: ChipAttributePath?, + eventPath: ChipEventPath?, + e: Exception + ) { + requireActivity().runOnUiThread { + Toast.makeText( + requireActivity(), + R.string.ota_provider_invalid_attribute, + Toast.LENGTH_SHORT + ) + .show() + } + } + + override fun onReport(nodeState: NodeState?) { + val attributeStates = + nodeState?.getEndpointState(endpointId)?.getClusterState(clusterId)?.attributeStates + ?: return + + requireActivity().runOnUiThread { + val description = attributeStates[ClusterIDMapping.ModeSelect.Attribute.Description.id] + binding.descriptionEd.setText(description?.value?.toString()) + + val standardNamespace = + attributeStates[ClusterIDMapping.ModeSelect.Attribute.StandardNamespace.id] + binding.standardNamespaceEd.setText(standardNamespace?.value?.toString()) + + val currentMode = attributeStates[ClusterIDMapping.ModeSelect.Attribute.CurrentMode.id] + binding.currentModeEd.setText(currentMode?.value?.toString()) + + setVisibility( + attributeStates[ClusterIDMapping.ModeSelect.Attribute.StartUpMode.id], + binding.startUpModeEd, + binding.startUpModeTv, + binding.startUpModeWriteBtn + ) + setVisibility( + attributeStates[ClusterIDMapping.ModeSelect.Attribute.OnMode.id], + binding.onModeEd, + binding.onModeTv, + binding.onModeWriteBtn + ) + + val supportedModesTlv = + attributeStates[ClusterIDMapping.ModeSelect.Attribute.SupportedModes.id]?.tlv + + supportedModesTlv?.let { + setSupportedModeSpinner(it, currentMode?.value?.toString()?.toUInt()) + } + } + } + }, + devicePtr, + listOf<ChipAttributePath>(path), + 0 + ) + } + + private fun setVisibility( + attribute: AttributeState?, + modeEd: EditText, + modeTv: TextView, + writeBtn: TextView + ) { + val modeVisibility = + if (attribute != null) { + modeEd.setText(attribute.value?.toString() ?: "NULL") + View.VISIBLE + } else { + View.GONE + } + modeEd.visibility = modeVisibility + modeTv.visibility = modeVisibility + writeBtn.visibility = modeVisibility + } + + private fun setSupportedModeSpinner(supportedModesTlv: ByteArray, currentModeValue: UInt?) { + var pos = 0 + var currentItemId = 0 + val modeOptionStructList: List<ModeSelectClusterModeOptionStruct> + TlvReader(supportedModesTlv).also { + modeOptionStructList = buildList { + it.enterArray(AnonymousTag) + while (!it.isEndOfContainer()) { + val struct = ModeSelectClusterModeOptionStruct.fromTlv(AnonymousTag, it) + add(struct) + if (currentModeValue != null && struct.mode == currentModeValue) { + currentItemId = pos + } + pos++ + } + it.exitContainer() + } + binding.supportedModesSp.adapter = + ArrayAdapter( + requireContext(), + android.R.layout.simple_spinner_dropdown_item, + modeOptionStructList.map { it.show() } + ) + binding.supportedModesSp.setSelection(currentItemId) + binding.currentModeEd.setText(binding.supportedModesSp.selectedItem.toString()) + } + } + + private suspend fun changeToModeBtnClick() { + val devicePtr = + try { + ChipClient.getConnectedDevicePointer(requireContext(), addressUpdateFragment.deviceId) + } catch (e: IllegalStateException) { + Log.d(TAG, "getConnectedDevicePointer exception", e) + showMessage("Get DevicePointer fail!") + return + } + ChipClusters.ModeSelectCluster(devicePtr, addressUpdateFragment.endpointId) + .changeToMode( + object : ChipClusters.DefaultClusterCallback { + override fun onError(error: java.lang.Exception?) { + Log.d(TAG, "onError", error) + showMessage("Error : ${error.toString()}") + } + + override fun onSuccess() { + showMessage("Change Success") + scope.launch { readAttributeBtnClick() } + } + }, + currentMode + ) + } + + private suspend fun writeAttributeBtnClick(attribute: ModeSelect.Attribute, value: UInt) { + val clusterId = ModeSelect.ID + val devicePtr = + try { + ChipClient.getConnectedDevicePointer(requireContext(), addressUpdateFragment.deviceId) + } catch (e: IllegalStateException) { + Log.d(TAG, "getConnectedDevicePointer exception", e) + showMessage("Get DevicePointer fail!") + return + } + deviceController.write( + object : WriteAttributesCallback { + override fun onError(attributePath: ChipAttributePath?, ex: java.lang.Exception?) { + showMessage("Write ${attribute.name} failure $ex") + Log.e(TAG, "Write ${attribute.name} failure", ex) + } + + override fun onResponse(attributePath: ChipAttributePath, status: Status) { + showMessage("Write ${attribute.name} response: $status") + } + }, + devicePtr, + listOf( + AttributeWriteRequest.newInstance( + addressUpdateFragment.endpointId, + clusterId, + attribute.id, + TlvWriter().put(AnonymousTag, value).getEncoded(), + Optional.empty() + ) + ), + 0, + 0 + ) + } + + private fun ModeSelectClusterModeOptionStruct.show(): String { + val value = this + return StringBuilder() + .apply { + append("${value.mode}-${value.label}") + append("[") + for (semanticTag in value.semanticTags) { + append("${semanticTag.value}:${semanticTag.mfgCode}") + append(",") + } + append("]") + } + .toString() + } + + override fun onDestroyView() { + super.onDestroyView() + deviceController.finishOTAProvider() + _binding = null + } + + private fun showMessage(msg: String) { + requireActivity().runOnUiThread { binding.commandStatusTv.text = msg } + } + + companion object { + private const val TAG = "ModeSelectClientFragment" + + fun newInstance(): ModeSelectClientFragment = ModeSelectClientFragment() + } +} diff --git a/examples/android/CHIPTool/app/src/main/res/layout/mode_select_fragment.xml b/examples/android/CHIPTool/app/src/main/res/layout/mode_select_fragment.xml new file mode 100644 index 00000000000000..3f13ca9b654d36 --- /dev/null +++ b/examples/android/CHIPTool/app/src/main/res/layout/mode_select_fragment.xml @@ -0,0 +1,219 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingStart="16dp" + android:paddingEnd="16dp" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <androidx.fragment.app.FragmentContainerView + android:id="@+id/addressUpdateFragment" + android:name="com.google.chip.chiptool.clusterclient.AddressUpdateFragment" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"/> + + <androidx.constraintlayout.helper.widget.Flow + android:id="@+id/flow" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + app:constraint_referenced_ids="descriptionTv, descriptionEd, descriptionEmptyTv, + standardNamespaceTv, standardNamespaceEd, standardNamespaceEmptyTv, + supportedModesTv, supportedModesSp, changeToModeBtn, + currentModeTv, currentModeEd, currentModeEmptyTv, + startUpModeTv, startUpModeEd, startUpModeWriteBtn, + onModeTv, onModeEd, onModeWriteBtn" + app:flow_horizontalBias="0.0" + app:flow_horizontalGap="8dp" + app:flow_horizontalStyle="packed" + app:flow_maxElementsWrap="3" + app:flow_wrapMode="aligned" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/addressUpdateFragment" /> + + <TextView + android:id="@+id/descriptionTv" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="16sp" + android:text="@string/mode_select_description_text" /> + + <EditText + android:id="@+id/descriptionEd" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:inputType="text" + android:hint="" + android:enabled="false" + tools:ignore="LabelFor" /> + + <TextView + android:id="@+id/descriptionEmptyTv" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="16sp" + android:text="" /> + + <TextView + android:id="@+id/standardNamespaceTv" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="16sp" + android:text="@string/mode_select_standard_namespace_text" /> + + <EditText + android:id="@+id/standardNamespaceEd" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:autofillHints="" + android:inputType="text" + android:digits="01234567890abcdefABCDEF" + android:hint="" + android:enabled="false" + tools:ignore="LabelFor" /> + + <TextView + android:id="@+id/standardNamespaceEmptyTv" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="16sp" + android:text="" /> + + <TextView + android:id="@+id/supportedModesTv" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="16sp" + android:text="@string/mode_select_supported_modes_text" /> + + <Spinner + android:id="@+id/supportedModesSp" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:autofillHints="" + android:inputType="text" + android:hint="" + tools:ignore="LabelFor" /> + + <TextView + android:id="@+id/changeToModeBtn" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="16sp" + android:background="@android:color/darker_gray" + android:text="@string/mode_select_change_to_mode_text" /> + + <TextView + android:id="@+id/currentModeTv" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="16sp" + android:text="@string/mode_select_current_mode_text" /> + + <EditText + android:id="@+id/currentModeEd" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:autofillHints="" + android:inputType="number" + android:hint="" + android:enabled="false" + tools:ignore="LabelFor" /> + + <TextView + android:id="@+id/currentModeEmptyTv" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="16sp" + android:text="" /> + + <TextView + android:id="@+id/startUpModeTv" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="16sp" + android:text="@string/mode_select_start_up_mode_text" /> + + <EditText + android:id="@+id/startUpModeEd" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:autofillHints="" + android:inputType="number" + android:hint="" + tools:ignore="LabelFor" /> + + <TextView + android:id="@+id/startUpModeWriteBtn" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="16sp" + android:background="@android:color/darker_gray" + android:text="@string/mode_select_write_text" /> + + <TextView + android:id="@+id/onModeTv" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="16sp" + android:text="@string/mode_select_on_mode_text" /> + + <EditText + android:id="@+id/onModeEd" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:autofillHints="" + android:inputType="number" + android:hint="" + tools:ignore="LabelFor" /> + + <TextView + android:id="@+id/onModeWriteBtn" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="16sp" + android:background="@android:color/darker_gray" + android:text="@string/mode_select_write_text" /> + + <TextView + android:id="@+id/readAttributeBtn" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:background="@android:color/darker_gray" + android:padding="8dp" + android:text="@string/mode_select_read_attribute_text" + android:textSize="16sp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/flow" /> + + <TextView + android:id="@+id/commandStatusTv" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="16dp" + android:minLines="4" + android:singleLine="false" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/readAttributeBtn" + android:textSize="16sp" /> +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/examples/android/CHIPTool/app/src/main/res/layout/select_action_fragment.xml b/examples/android/CHIPTool/app/src/main/res/layout/select_action_fragment.xml index db999600999741..e2f5ce8f1d7db0 100644 --- a/examples/android/CHIPTool/app/src/main/res/layout/select_action_fragment.xml +++ b/examples/android/CHIPTool/app/src/main/res/layout/select_action_fragment.xml @@ -143,6 +143,14 @@ android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:text="@string/icd_btn_text" /> + + <Button + android:id="@+id/modeSelectBtn" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:text="@string/mode_select_btn_text" /> </LinearLayout> </ScrollView> diff --git a/examples/android/CHIPTool/app/src/main/res/values/strings.xml b/examples/android/CHIPTool/app/src/main/res/values/strings.xml index 5c9e3d0e425f1d..0d01e003160854 100644 --- a/examples/android/CHIPTool/app/src/main/res/values/strings.xml +++ b/examples/android/CHIPTool/app/src/main/res/values/strings.xml @@ -292,6 +292,17 @@ <string name="ota_provider_write_text">Write</string> <string name="ota_provider_node_id_text">Node ID</string> + <string name="mode_select_btn_text">Mode Select</string> + <string name="mode_select_description_text">Description</string> + <string name="mode_select_standard_namespace_text">Standard Namespace</string> + <string name="mode_select_supported_modes_text">Supported Modes</string> + <string name="mode_select_current_mode_text">CurrentMode</string> + <string name="mode_select_start_up_mode_text">StartUpMode</string> + <string name="mode_select_on_mode_text">OnMode</string> + <string name="mode_select_read_attribute_text">Read</string> + <string name="mode_select_change_to_mode_text">Change</string> + <string name="mode_select_write_text">Write</string> + <string name="icd_btn_text">Intermittently Connected Device</string> <string name="icd_registration_completed">ICD device registration completed, How to trigger to switch to Active Mode : %1$s</string> </resources>