diff --git a/.github/.wordlist.txt b/.github/.wordlist.txt index 9d916059a94470..384e1d46451d99 100644 --- a/.github/.wordlist.txt +++ b/.github/.wordlist.txt @@ -757,6 +757,7 @@ js json JTAG Jupyter +judgement jupyterlab KA kAdminister diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index dad1d2c6463644..b889c7733a847c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,7 +6,9 @@ > If you do not have an issue number, please have a good description of > the problem and the fix. Help the reviewer understand what to expect. > +> Add a `### Testing` section to your PR to describe how testing was done. +> See +> > Make sure you delete these instructions (to prove you have read them). > > !!!!!!!!!! Instructions end - diff --git a/.github/labeler.yml b/.github/labeler.yml index 68d976ecd5b0df..cb4d0839816965 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -119,15 +119,6 @@ test driver: - src/test_driver/* - src/test_driver/**/* -# Cert tests touched: add current milestone delta-tracking label. -# TODO: Change after Aug 15, 2024 -matter-1.4-te2-script-change: - - changed-files: - - any-glob-to-any-file: - - src/python_testing/* - - src/python_testing/**/* - - src/app/tests/suites/certification/* - ############################################################ # Source Code ############################################################ diff --git a/.github/workflows/cancel_workflows_for_pr.yaml b/.github/workflows/cancel_workflows_for_pr.yaml index caba07bedff6fa..f3c6c4967e317d 100644 --- a/.github/workflows/cancel_workflows_for_pr.yaml +++ b/.github/workflows/cancel_workflows_for_pr.yaml @@ -49,4 +49,5 @@ jobs: --require "Lint Code Base" \ --require "ZAP" \ --require "Run misspell" \ + --require "PR validity" \ --max-pr-age-minutes 20 diff --git a/.github/workflows/examples-nxp.yaml b/.github/workflows/examples-nxp.yaml index 45a1971baf7286..d74f01bc813ccb 100644 --- a/.github/workflows/examples-nxp.yaml +++ b/.github/workflows/examples-nxp.yaml @@ -40,7 +40,7 @@ jobs: if: github.actor != 'restyled-io[bot]' container: - image: ghcr.io/project-chip/chip-build-nxp:94 + image: ghcr.io/project-chip/chip-build-nxp:96 volumes: - "/tmp/bloat_reports:/tmp/bloat_reports" steps: @@ -203,7 +203,6 @@ jobs: scripts/run_in_build_env.sh "\ ./scripts/build/build_examples.py \ --target nxp-rw61x-freertos-all-clusters-wifi \ - --target nxp-rw61x-freertos-all-clusters-wifi-ota-cmake \ build \ --copy-artifacts-to out/artifacts \ " diff --git a/.github/workflows/java-tests.yaml b/.github/workflows/java-tests.yaml index df39538cbeced9..7d5170c39bc5a3 100644 --- a/.github/workflows/java-tests.yaml +++ b/.github/workflows/java-tests.yaml @@ -94,6 +94,7 @@ jobs: --target linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test \ --target linux-x64-java-matter-controller \ --target linux-x64-lit-icd-ipv6only \ + --target linux-x64-ota-requestor \ build \ " - name: Build Kotlin Matter Controller @@ -259,6 +260,19 @@ jobs: --tool-args "onnetwork-long --nodeid 1 --setup-pin-code 20202021 --discriminator 3840 -t 1000" \ --factoryreset \ ' + - name: Run Pairing ICD Onnetwork and OTA test + # Generally completes in seconds + timeout-minutes: 2 + run: | + scripts/run_in_python_env.sh out/venv \ + './scripts/tests/run_java_test.py \ + --app out/linux-x64-ota-requestor/chip-ota-requestor-app \ + --app-args "--discriminator 3840 --interface-id -1" \ + --tool-path out/linux-x64-java-matter-controller \ + --tool-cluster "ota" \ + --tool-args "onnetwork-long-ota-over-bdx --nodeid 1 --setup-pin-code 20202021 --discriminator 3840 -t 1000" \ + --factoryreset \ + ' - name: Run Pairing Onnetwork and get diagnostic log Test run: | scripts/run_in_python_env.sh out/venv \ diff --git a/.github/workflows/pr-validation.yaml b/.github/workflows/pr-validation.yaml new file mode 100644 index 00000000000000..a0e6c22cdf8d69 --- /dev/null +++ b/.github/workflows/pr-validation.yaml @@ -0,0 +1,62 @@ +name: PR validity + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +jobs: + check_testing_header: + runs-on: ubuntu-latest + steps: + + - name: Check for `### Testing` section in PR + id: check-testing + continue-on-error: true + run: | + python -c 'import sys; pr_summary = """${{ github.event.pull_request.body }}"""; sys.exit(0 if "### Testing" in pr_summary else 1)' + + - name: Check for PR starting instructions + id: check-instructions + continue-on-error: true + run: | + python -c 'import sys; pr_summary = """${{ github.event.pull_request.body }}"""; sys.exit(1 if "Make sure you delete these instructions" in pr_summary else 0)' + + # NOTE: comments disabled for now as separate permissions are required + # failing CI step may be sufficient to start (although it contains less information about why it failed) + + # - name: Add comment (missing instructions) + # if: steps.check-instructions.outcome == 'failure' + # uses: actions/github-script@v6 + # with: + # github-token: ${{ secrets.GITHUB_TOKEN }} + # script: | + # github.rest.issues.createComment({ + # issue_number: context.issue.number, + # owner: context.repo.owner, + # repo: context.repo.repo, + # body: 'Please make sure to delete starter instructions from your PR summary and replace them with a descriptive summary.' + # }) + + - name: Fail if PR instructions were not deleted + if: steps.check-instructions.outcome == 'failure' + run: | + python -c 'import sys; print("PR instructions were not replaced"); sys.exit(1)' + + # - name: Add comment (missing testing) + # if: steps.check-testing.outcome == 'failure' + # uses: actions/github-script@v6 + # with: + # github-token: ${{ secrets.GITHUB_TOKEN }} + # script: | + # github.rest.issues.createComment({ + # issue_number: context.issue.number, + # owner: context.repo.owner, + # repo: context.repo.repo, + # body: 'Please add a `### Testing` section to your PR summary describing the testing performed. See https://github.com/project-chip/connectedhomeip/blob/master/CONTRIBUTING.md#pull-requests' + # }) + + - name: Fail if `### Testing` section not in PR + if: steps.check-testing.outcome == 'failure' + run: | + python -c 'import sys; print("Testing section missing (test failed)"); sys.exit(1)' + diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 04be85efe5067e..1ed2a01bdbc018 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -111,6 +111,7 @@ jobs: src/app/zap-templates/zcl/data-model/chip/boolean-state-cluster.xml \ src/app/zap-templates/zcl/data-model/chip/actions-cluster.xml \ src/app/zap-templates/zcl/data-model/chip/bridged-device-basic-information.xml \ + src/app/zap-templates/zcl/data-model/chip/camera-av-settings-user-level-management-cluster.xml \ src/app/zap-templates/zcl/data-model/chip/camera-av-stream-management-cluster.xml \ src/app/zap-templates/zcl/data-model/chip/chip-ota.xml \ src/app/zap-templates/zcl/data-model/chip/chip-types.xml \ @@ -185,6 +186,7 @@ jobs: src/app/zap-templates/zcl/data-model/chip/software-diagnostics-cluster.xml \ src/app/zap-templates/zcl/data-model/chip/switch-cluster.xml \ src/app/zap-templates/zcl/data-model/chip/target-navigator-cluster.xml \ + src/app/zap-templates/zcl/data-model/chip/temperature-control-cluster.xml \ src/app/zap-templates/zcl/data-model/chip/temperature-measurement-cluster.xml \ src/app/zap-templates/zcl/data-model/chip/test-cluster.xml \ src/app/zap-templates/zcl/data-model/chip/thermostat-user-interface-configuration-cluster.xml \ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a71219f560671b..3cea69412beded 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -160,6 +160,99 @@ This will trigger the continuous-integration checks. You can view the results in the respective services. Note that the integration checks will report failures on occasion. +#### Pull requests + +Aim to make pull requests easy to read both when viewed in a list (title only) +as well as clear in content within the description. + +##### Title formatting + +Describe the change as a one-line in some descriptive manner. Add sufficient +context for a reader to understand what is improved. If platform-specific +consider adding the platform as a prefix, like `[Android]` or any other tags may +be useful for quick filtering like `[TC-ABC-1.2]` to tag test changes. + +Examples of descriptive titles: + +- `[Silabs] Fix compile of SiWx917 if LED and BUTTON are disabled` +- `[Telink] Update build Dockerfile with new Zeprhy SHA: c05c4.....` +- `General Commissioning Cluster: use AttributeAccessInterface/CommandHandlerInterface for processing` +- `Scenes Management/CopyScene: set access as manage instead of default to match the spec` +- `Fix build errors due to ChipDeviceEvent default constructor not being available` +- `Fix crash during DNSSD processing due to malformed packet` +- `[NRF] Fix crash due to stack overflow during logging for PW-RPC builds` +- `[TC-ABC-2.3] added new python test case based on test plan` +- `[TC-ABC] migrate tests from yaml to python` + +Examples of titles that are vague (not clear what the change is, one would need +to open the pull request for details or open additional issue in GitHub) + +- `Work on issue 1234` +- `Fix android JniTypeWrappers` +- `Fix segfault in BLE` +- `Fix TC-ABC-1.2` +- `Update Readme` + +##### Summary contents + +Ensure that there is sufficient detail in issue summaries to make the content of +the PR clear: + +- a `TLDR` of the change content. This is a judgement call on details, + generally you should include a what was changed and why. The change is + trivial/short, this can be very short (i.e. "fixed typos" is perfectly + acceptable, however if changing 100-1000s of line, the areas of changes + should be explained) +- If a crash/error is fixed, explain the root cause and if the fix is not + obvious (again, judgement call), explain why the given approach was taken. +- Help the reviewer out with any notable information (specific platform + issues, extra thoughts or requests for feedback or gotchas on tricky code, + followup work or PR dependencies) +- TIP: use the syntax of `Fixes #....` to mark issues completed on PR merge or + use `#...` to reference issues that are addressed. +- TIP: prefer adding some brief description (especially about the content of + the changes) instead of just referencing an issue (helps reviewers get + context faster without extra clicks). + +##### Testing section + +All Pull Requests **MUST** contain a `#### Testing` section that describes how +the pull request was tested. Ideally every test should have automated testing, +however for platform specific changes or hardware-specific issues we may not be +able to have such tests (e.g. we may not BLE or NFC capability in CI). As such, +manual testing is acceptable, however the description has to be detailed +intentionally to avoid a bias towards marking pull requests as "manually tested" +out of convenience. + +- Automated testing + + **AWESOME**. You can say "unit tests added/updated" or "Integration tests + updated to cover functionality" or "existing tests already cover this" (make + sure they do. Integration tests often only cover happy paths). + + Add any notes on not covered things. It is a judgement call on how much can + be covered as 100% sounds great however not always possible. + +- Manual testing + + Describe why automated testing is impossible in the current CI environment + or difficult to add. If adding later, reference the issue to add automation + and a timeline for adding such automation. + + Describe in **DETAIL** how manual testing was done: what environment, what + builds were used (`build-example` names are ok such as + `flashed qpg-qpg6105-light` and `used linux-x64-chip-tool-clang`). Describe + commands ran (often chip-tool) and physical interaction and what was + observed. + +- Trivial/obvious change + + In rare cases the change is trivial (e.g. fixing a typo in a `Readme.md`). + Scripts still require a `#### Testing` section however you can be brief like + `N/A` or `checked new URL opens`. Note that these cases are rare - e.g. + fixing a typo in an ID still requires some description on how you checked + that the new ID takes effect. + ### Review Requirements #### Documentation Best Practices diff --git a/docs/ids_and_codes/zap_clusters.md b/docs/ids_and_codes/zap_clusters.md index b37e8b2b8b7781..a805a9436c29dd 100644 --- a/docs/ids_and_codes/zap_clusters.md +++ b/docs/ids_and_codes/zap_clusters.md @@ -131,6 +131,7 @@ Generally regenerate using one of: | 1296 | 0x510 | ContentAppObserver | | 1360 | 0x550 | ZoneManagement | | 1361 | 0x551 | CameraAvStreamManagement | +| 1362 | 0x552 | CameraAvSettingsUserLevelManagement | | 1363 | 0x553 | WebRTCTransportProvider | | 1364 | 0x554 | WebRTCTransportRequestor | | 1366 | 0x556 | Chime | 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(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 + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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" /> + +