diff --git a/.github/workflows/java-tests.yaml b/.github/workflows/java-tests.yaml
index 504cb865a9cc0b..473cd482357a75 100644
--- a/.github/workflows/java-tests.yaml
+++ b/.github/workflows/java-tests.yaml
@@ -236,6 +236,17 @@ jobs:
--tool-args "onnetwork-long --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 \
+ './scripts/tests/run_java_test.py \
+ --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app \
+ --app-args "--discriminator 3840 --interface-id -1 --crash_log ./crashLog.log --end_user_support_log ./enduser.log --network_diagnostics_log ./network.log" \
+ --tool-path out/linux-x64-java-matter-controller \
+ --tool-cluster "bdx" \
+ --tool-args "onnetwork-long-downloadLog --nodeid 1 --setup-pin-code 20202021 --discriminator 3840 -t 3000 --logType CrashLogs --fileName ./crashLog.log" \
+ --factoryreset \
+ '
- name: Run Pairing Onnetwork Test
run: |
scripts/run_in_python_env.sh out/venv \
diff --git a/examples/android/CHIPTool/app/src/main/AndroidManifest.xml b/examples/android/CHIPTool/app/src/main/AndroidManifest.xml
index 0526dc5a78160d..efd089817efe68 100644
--- a/examples/android/CHIPTool/app/src/main/AndroidManifest.xml
+++ b/examples/android/CHIPTool/app/src/main/AndroidManifest.xml
@@ -46,6 +46,15 @@
+
+
+
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 69bfdc109912c9..fb8141e72d33a8 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
@@ -72,6 +72,7 @@ class SelectActionFragment : Fragment() {
binding.provisionCustomFlowBtn.setOnClickListener { handleProvisionCustomFlowClicked() }
binding.wildcardBtn.setOnClickListener { handleWildcardClicked() }
binding.unpairDeviceBtn.setOnClickListener { handleUnpairDeviceClicked() }
+ binding.diagnosticLogBtn.setOnClickListener { handleDiagnosticLogClicked() }
binding.groupSettingBtn.setOnClickListener { handleGroupSettingClicked() }
binding.otaProviderBtn.setOnClickListener { handleOTAProviderClicked() }
binding.icdBtn.setOnClickListener { handleICDClicked() }
@@ -225,6 +226,10 @@ class SelectActionFragment : Fragment() {
showFragment(OtaProviderClientFragment.newInstance())
}
+ private fun handleDiagnosticLogClicked() {
+ showFragment(DiagnosticLogFragment.newInstance())
+ }
+
/** Notifies listener of provision-WiFi-credentials button click. */
private fun handleProvisionWiFiCredentialsClicked() {
getCallback()?.setNetworkType(ProvisionNetworkType.WIFI)
diff --git a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/DiagnosticLogFragment.kt b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/DiagnosticLogFragment.kt
new file mode 100644
index 00000000000000..ea7d5438d247c3
--- /dev/null
+++ b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/DiagnosticLogFragment.kt
@@ -0,0 +1,172 @@
+package com.google.chip.chiptool.clusterclient
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.os.Environment
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import androidx.core.content.FileProvider
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import chip.devicecontroller.ChipDeviceController
+import chip.devicecontroller.DiagnosticLogType
+import chip.devicecontroller.DownloadLogCallback
+import com.google.chip.chiptool.ChipClient
+import com.google.chip.chiptool.R
+import com.google.chip.chiptool.databinding.DiagnosticLogFragmentBinding
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+class DiagnosticLogFragment : Fragment() {
+ private val deviceController: ChipDeviceController
+ get() = ChipClient.getDeviceController(requireContext())
+
+ private lateinit var scope: CoroutineScope
+
+ private lateinit var addressUpdateFragment: AddressUpdateFragment
+
+ private var _binding: DiagnosticLogFragmentBinding? = null
+ private val binding
+ get() = _binding!!
+
+ private val timeout: Long
+ get() = binding.timeoutEd.text.toString().toULongOrNull()?.toLong() ?: 0L
+
+ private val diagnosticLogTypeList = DiagnosticLogType.values()
+ private val diagnosticLogType: DiagnosticLogType
+ get() = diagnosticLogTypeList[binding.diagnosticTypeSp.selectedItemPosition]
+
+ private var mDownloadFile: File? = null
+ private var mDownloadFileOutputStream: FileOutputStream? = null
+
+ private var mReceiveFileLen = 0U
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = DiagnosticLogFragmentBinding.inflate(inflater, container, false)
+ scope = viewLifecycleOwner.lifecycleScope
+
+ addressUpdateFragment =
+ childFragmentManager.findFragmentById(R.id.addressUpdateFragment) as AddressUpdateFragment
+
+ binding.getDiagnosticLogBtn.setOnClickListener { scope.launch { getDiagnosticLogClick() } }
+
+ binding.diagnosticTypeSp.adapter =
+ ArrayAdapter(
+ requireContext(),
+ android.R.layout.simple_spinner_dropdown_item,
+ diagnosticLogTypeList
+ )
+
+ return binding.root
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ inner class ChipDownloadLogCallback : DownloadLogCallback {
+ override fun onError(fabricIndex: Int, nodeId: Long, errorCode: Long) {
+ Log.d(TAG, "onError: $fabricIndex, ${nodeId.toULong()}, $errorCode")
+ showMessage("Downloading Failed")
+ mDownloadFileOutputStream?.flush() ?: return
+ }
+
+ override fun onSuccess(fabricIndex: Int, nodeId: Long) {
+ Log.d(TAG, "onSuccess: $fabricIndex, ${nodeId.toULong()}")
+ mDownloadFileOutputStream?.flush() ?: return
+ showMessage("Downloading Completed")
+ mDownloadFile?.let { showNotification(it) } ?: return
+ }
+
+ override fun onTransferData(fabricIndex: Int, nodeId: Long, data: ByteArray): Boolean {
+ Log.d(TAG, "onTransferData : ${data.size}")
+ if (mDownloadFileOutputStream == null) {
+ Log.d(TAG, "mDownloadFileOutputStream or mDownloadFile is null")
+ return false
+ }
+ return addData(mDownloadFileOutputStream!!, data)
+ }
+
+ private fun addData(outputStream: FileOutputStream, data: ByteArray): Boolean {
+ try {
+ outputStream.write(data)
+ } catch (e: IOException) {
+ Log.d(TAG, "IOException", e)
+ return false
+ }
+ mReceiveFileLen += data.size.toUInt()
+ showMessage("Receive Data Size : $mReceiveFileLen")
+ return true
+ }
+ }
+
+ private fun getDiagnosticLogClick() {
+ mDownloadFile =
+ createLogFile(
+ deviceController.fabricIndex.toUInt(),
+ addressUpdateFragment.deviceId.toULong(),
+ diagnosticLogType
+ )
+ mDownloadFileOutputStream = FileOutputStream(mDownloadFile)
+ deviceController.downloadLogFromNode(
+ addressUpdateFragment.deviceId,
+ diagnosticLogType,
+ timeout,
+ ChipDownloadLogCallback()
+ )
+ }
+
+ private fun isExternalStorageWritable(): Boolean {
+ return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
+ }
+
+ private fun createLogFile(fabricIndex: UInt, nodeId: ULong, type: DiagnosticLogType): File? {
+ if (!isExternalStorageWritable()) {
+ return null
+ }
+ val now = System.currentTimeMillis()
+ val fileName = "${type}_${fabricIndex}_${nodeId}_$now.txt"
+ mReceiveFileLen = 0U
+ return File(requireContext().getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), fileName)
+ }
+
+ private fun showNotification(file: File) {
+ val intent =
+ Intent(Intent.ACTION_VIEW).apply {
+ setDataAndType(getFileUri(file), "text/plain")
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+
+ requireActivity().startActivity(intent)
+ }
+
+ private fun getFileUri(file: File): Uri {
+ return FileProvider.getUriForFile(
+ requireContext(),
+ "${requireContext().packageName}.provider",
+ file
+ )
+ }
+
+ private fun showMessage(msg: String) {
+ requireActivity().runOnUiThread { binding.diagnosticLogTv.text = msg }
+ }
+
+ companion object {
+ private const val TAG = "DiagnosticLogFragment"
+
+ fun newInstance(): DiagnosticLogFragment = DiagnosticLogFragment()
+ }
+}
diff --git a/examples/android/CHIPTool/app/src/main/res/layout/diagnostic_log_fragment.xml b/examples/android/CHIPTool/app/src/main/res/layout/diagnostic_log_fragment.xml
new file mode 100644
index 00000000000000..56b770a645b442
--- /dev/null
+++ b/examples/android/CHIPTool/app/src/main/res/layout/diagnostic_log_fragment.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 5824e8061a4e03..db999600999741 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
@@ -112,6 +112,14 @@
android:layout_marginTop="8dp"
android:text="@string/unpair_device_btn_text" />
+
+