blob: 13caa7b50ea09a0eada0e5d24fe9a4497d6bad24 [file] [log] [blame]
package com.google.chip.chiptool.clusterclient
import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.OpenableColumns
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.EditText
import android.widget.Spinner
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import chip.devicecontroller.ChipClusters
import chip.devicecontroller.ChipClusters.DefaultClusterCallback
import chip.devicecontroller.ChipDeviceController
import chip.devicecontroller.ClusterIDMapping
import chip.devicecontroller.OTAProviderDelegate
import chip.devicecontroller.OTAProviderDelegate.QueryImageResponseStatusEnum
import chip.devicecontroller.ReportCallback
import chip.devicecontroller.WriteAttributesCallback
import chip.devicecontroller.cluster.structs.AccessControlClusterAccessControlEntryStruct
import chip.devicecontroller.cluster.structs.OtaSoftwareUpdateRequestorClusterProviderLocation
import chip.devicecontroller.model.AttributeWriteRequest
import chip.devicecontroller.model.ChipAttributePath
import chip.devicecontroller.model.ChipEventPath
import chip.devicecontroller.model.NodeState
import chip.devicecontroller.model.Status
import com.google.chip.chiptool.ChipClient
import com.google.chip.chiptool.GenericChipDeviceListener
import com.google.chip.chiptool.R
import com.google.chip.chiptool.databinding.OtaProviderClientFragmentBinding
import com.google.chip.chiptool.util.toAny
import java.io.BufferedInputStream
import java.io.IOException
import java.io.InputStream
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 OtaProviderClientFragment : Fragment() {
private val deviceController: ChipDeviceController
get() = ChipClient.getDeviceController(requireContext())
private lateinit var scope: CoroutineScope
private lateinit var addressUpdateFragment: AddressUpdateFragment
private var _binding: OtaProviderClientFragmentBinding? = null
private var fileUri: Uri? = null
private val vendorId: Int
get() = binding.vendorIdEd.text.toString().toInt()
private val otaProviderCallback = OtaProviderCallback()
private val binding
get() = _binding!!
private val attributeList = ClusterIDMapping.OtaSoftwareUpdateRequestor.Attribute.values()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = OtaProviderClientFragmentBinding.inflate(inflater, container, false)
scope = viewLifecycleOwner.lifecycleScope
deviceController.setCompletionListener(ChipControllerCallback())
addressUpdateFragment =
childFragmentManager.findFragmentById(R.id.addressUpdateFragment) as AddressUpdateFragment
binding.selectFirmwareFileBtn.setOnClickListener { selectFirmwareFileBtnClick() }
binding.updateOTAStatusBtn.setOnClickListener { updateOTAStatusBtnClick() }
binding.announceOTAProviderBtn.setOnClickListener {
scope.launch { sendAnnounceOTAProviderBtnClick() }
}
binding.writeAclBtn.setOnClickListener { scope.launch { sendAclBtnClick() } }
binding.readAttributeBtn.setOnClickListener { scope.launch { readAttributeBtnClick() } }
binding.writeAttributeBtn.setOnClickListener { scope.launch { writeAttributeBtnClick() } }
setQueryImageSpinnerListener()
val attributeNames = attributeList.map { it.name }
binding.attributeSp.adapter =
ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, attributeNames)
binding.vendorIdEd.setText(ChipClient.VENDOR_ID.toString())
binding.delayActionTimeEd.setText("0")
deviceController.startOTAProvider(otaProviderCallback)
return binding.root
}
private suspend fun sendAclBtnClick() {
val endpointId = 0
val clusterId = ClusterIDMapping.AccessControl.ID
val attributeId = ClusterIDMapping.AccessControl.Attribute.Acl.id
val attributePath = 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
) {
Log.d(TAG, "onError : ", e)
showMessage("Error : $e")
}
override fun onReport(nodeState: NodeState?) {
Log.d(TAG, "onResponse")
val tlv =
nodeState
?.getEndpointState(endpointId)
?.getClusterState(clusterId)
?.getAttributeState(attributeId)
?.tlv
requireActivity().runOnUiThread { showAddAccessControlDialog(tlv) }
}
},
devicePtr,
listOf(attributePath),
0
)
}
private fun showAddAccessControlDialog(tlv: ByteArray?) {
if (tlv == null) {
Log.d(TAG, "Access Control read fail")
showMessage("Access Control read fail")
return
}
val dialogView =
requireActivity().layoutInflater.inflate(R.layout.add_access_control_dialog, null)
val groupIdEd = dialogView.findViewById<EditText>(R.id.groupIdEd)
groupIdEd.visibility = View.GONE
val nodeIdEd = dialogView.findViewById<EditText>(R.id.nodeIdEd)
nodeIdEd.visibility = View.VISIBLE
val accessControlEntrySp = dialogView.findViewById<Spinner>(R.id.accessControlEntrySp)
accessControlEntrySp.adapter =
ArrayAdapter(
requireContext(),
android.R.layout.simple_spinner_dropdown_item,
GroupSettingFragment.Companion.AccessControlEntry.values()
)
val dialog = AlertDialog.Builder(requireContext()).apply { setView(dialogView) }.create()
dialogView.findViewById<Button>(R.id.addAccessControlBtn).setOnClickListener {
scope.launch {
sendAccessControl(
tlv,
nodeIdEd.text.toString().toULong(),
GroupSettingFragment.Companion.AccessControlEntry.valueOf(
accessControlEntrySp.selectedItem.toString()
)
.id
)
requireActivity().runOnUiThread { dialog.dismiss() }
}
}
dialog.show()
}
private suspend fun sendAccessControl(tlv: ByteArray, nodeId: ULong, privilege: UInt) {
val tlvWriter = TlvWriter().startArray(AnonymousTag)
var entryStructList: List<AccessControlClusterAccessControlEntryStruct>
TlvReader(tlv).also {
entryStructList = buildList {
it.enterArray(AnonymousTag)
while (!it.isEndOfContainer()) {
add(AccessControlClusterAccessControlEntryStruct.fromTlv(AnonymousTag, it))
}
it.exitContainer()
}
}
// If GroupID is already added to AccessControl, do not add it.
for (entry in entryStructList) {
if (
entry.authMode == 2U /* CASE */ &&
entry.subjects != null &&
entry.subjects!!.contains(nodeId)
) {
continue
}
entry.toTlv(AnonymousTag, tlvWriter)
}
val newEntry =
AccessControlClusterAccessControlEntryStruct(
privilege,
2U /* CASE */,
listOf(nodeId),
null,
deviceController.fabricIndex.toUInt()
)
newEntry.toTlv(AnonymousTag, tlvWriter)
tlvWriter.endArray()
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?, e: Exception?) {
Log.d(TAG, "onError : ", e)
showMessage("Error : ${e.toString()}")
}
override fun onResponse(attributePath: ChipAttributePath, status: Status) {
Log.d(TAG, "onResponse")
showMessage("$attributePath : Write response: $status")
}
},
devicePtr,
listOf(
AttributeWriteRequest.newInstance(
0,
ClusterIDMapping.AccessControl.ID,
ClusterIDMapping.AccessControl.Attribute.Acl.id,
tlvWriter.getEncoded()
)
),
0,
0
)
}
private suspend fun readAttributeBtnClick() {
val attribute = attributeList[binding.attributeSp.selectedItemPosition]
val endpointId = OTA_REQUESTER_ENDPOINT_ID
val clusterId = ClusterIDMapping.OtaSoftwareUpdateRequestor.ID
val attributeId = attribute.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 tlv =
nodeState
?.getEndpointState(endpointId)
?.getClusterState(clusterId)
?.getAttributeState(attributeId)
?.tlv
val value = tlv?.let { TlvReader(it).toAny() }
Log.i(TAG, "OtaSoftwareUpdateRequestor ${attribute.name} value: $value")
showMessage("OtaSoftwareUpdateRequestor ${attribute.name} value: $value")
}
},
devicePtr,
listOf<ChipAttributePath>(path),
0
)
}
private fun writeAttributeBtnClick() {
val attribute = attributeList[binding.attributeSp.selectedItemPosition]
if (attribute != ClusterIDMapping.OtaSoftwareUpdateRequestor.Attribute.DefaultOTAProviders) {
requireActivity().runOnUiThread {
Toast.makeText(
requireActivity(),
R.string.ota_provider_invalid_attribute,
Toast.LENGTH_SHORT
)
.show()
}
return
}
val dialogView =
requireActivity().layoutInflater.inflate(R.layout.write_default_otaproviders_dialog, null)
val fabricIndexEd = dialogView.findViewById<EditText>(R.id.fabricIndexEd)
val providerNodeIdEd = dialogView.findViewById<EditText>(R.id.providerNodeIdEd)
val endpointIdEd = dialogView.findViewById<EditText>(R.id.endpointIdEd)
fabricIndexEd.setText(deviceController.fabricIndex.toUInt().toString())
providerNodeIdEd.setText(deviceController.controllerNodeId.toULong().toString())
endpointIdEd.setText(OTA_PROVIDER_ENDPOINT_ID.toUInt().toString())
val dialog = AlertDialog.Builder(requireContext()).apply { setView(dialogView) }.create()
dialogView.findViewById<Button>(R.id.writeDefaultOtaProvidersBtn).setOnClickListener {
scope.launch {
sendWriteDefaultOTAProviders(
providerNodeIdEd.text.toString().toULong(),
endpointIdEd.text.toString().toUInt(),
fabricIndexEd.text.toString().toUInt()
)
requireActivity().runOnUiThread { dialog.dismiss() }
}
}
dialog.show()
}
private suspend fun sendWriteDefaultOTAProviders(
providerNodeId: ULong,
endpointId: UInt,
fabricIndex: UInt
) {
val endpoint = OTA_REQUESTER_ENDPOINT_ID
val clusterId = ClusterIDMapping.OtaSoftwareUpdateRequestor.ID
val attributeId = ClusterIDMapping.OtaSoftwareUpdateRequestor.Attribute.DefaultOTAProviders.id
val tlv =
TlvWriter()
.apply {
startArray(AnonymousTag)
OtaSoftwareUpdateRequestorClusterProviderLocation(providerNodeId, endpointId, fabricIndex)
.toTlv(AnonymousTag, this)
endArray()
}
.getEncoded()
val writeRequest = AttributeWriteRequest.newInstance(endpoint, clusterId, attributeId, tlv)
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?, e: Exception?) {
Log.d(TAG, "onError")
showMessage("error : ${e.toString()}")
}
override fun onResponse(attributePath: ChipAttributePath, status: Status) {
Log.d(TAG, "onResponse")
showMessage("$attributePath : Write response: $status")
}
},
devicePtr,
listOf<AttributeWriteRequest>(writeRequest),
0,
0
)
}
private fun setQueryImageSpinnerListener() {
val statusList = QueryImageResponseStatusEnum.values()
binding.titleStatusSp.adapter =
ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, statusList)
binding.titleStatusSp.onItemSelectedListener =
object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val isBusy = statusList[position] == QueryImageResponseStatusEnum.Busy
requireActivity().runOnUiThread {
binding.delayActionTimeTv.visibility =
if (isBusy) {
View.VISIBLE
} else {
View.GONE
}
binding.delayActionTimeEd.visibility = binding.delayActionTimeTv.visibility
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
Log.d(TAG, "onNothingSelected")
}
}
}
private fun selectFirmwareFileBtnClick() {
startActivityForResult(
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
},
REQUEST_CODE_OPEN_SAF
)
}
private fun getInputStream(uri: Uri?): InputStream? {
if (uri == null) {
return null
}
return requireContext().contentResolver.openInputStream(uri)
}
private fun queryName(uri: Uri?): String? {
if (uri == null) {
return null
}
val cursor = requireContext().contentResolver.query(uri, null, null, null, null)
return cursor?.let { c ->
val nameIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
c.moveToFirst()
val name = cursor.getString(nameIndex)
c.close()
name
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent)
if (resultCode != Activity.RESULT_OK || intent == null) {
return
}
val uri = intent.data
if (uri == null) {
Log.d(TAG, "onActivityResult : null")
return
}
val filename = queryName(uri)
fileUri = uri
requireActivity().runOnUiThread { binding.firmwareFileTv.text = filename }
}
private fun updateOTAStatusBtnClick() {
val version = 2L
val versionString = "2.0"
val filename = binding.firmwareFileTv.text.toString()
Log.d(TAG, "updateOTAStatusBtnClick : $filename")
when (binding.titleStatusSp.selectedItem.toString()) {
QueryImageResponseStatusEnum.UpdateAvailable.name ->
otaProviderCallback.setOTAFile(version, versionString, filename, fileUri)
QueryImageResponseStatusEnum.Busy.name ->
otaProviderCallback.setOTABusyError(
binding.delayActionTimeEd.text.toString().toUInt(),
binding.titleUserConsentNeededSp.selectedItem.toString().toBoolean()
)
QueryImageResponseStatusEnum.NotAvailable.name ->
otaProviderCallback.setOTANotAvailableError(
binding.titleUserConsentNeededSp.selectedItem.toString().toBoolean()
)
}
}
private suspend fun sendAnnounceOTAProviderBtnClick() {
requireActivity().runOnUiThread { updateOTAStatusBtnClick() }
val devicePtr =
try {
ChipClient.getConnectedDevicePointer(requireContext(), addressUpdateFragment.deviceId)
} catch (e: IllegalStateException) {
Log.d(TAG, "getConnectedDevicePointer exception", e)
showMessage("Get DevicePointer fail!")
return
}
val otaRequestCluster =
ChipClusters.OtaSoftwareUpdateRequestorCluster(devicePtr, OTA_REQUESTER_ENDPOINT_ID)
otaRequestCluster.announceOTAProvider(
object : DefaultClusterCallback {
override fun onSuccess() {
showMessage("announceOTAProvider command success")
Log.e(TAG, "announceOTAProvider command success")
}
override fun onError(ex: java.lang.Exception?) {
showMessage("announceOTAProvider command failure $ex")
Log.e(TAG, "announceOTAProvider command failure", ex)
}
},
deviceController.controllerNodeId.toULong().toLong(),
vendorId,
0 /* AnnounceReason */,
Optional.empty(),
OTA_PROVIDER_ENDPOINT_ID
)
}
override fun onDestroyView() {
super.onDestroyView()
deviceController.finishOTAProvider()
_binding = null
}
inner class OtaProviderCallback : OTAProviderDelegate {
private var fileName: String? = null
private var version: Long = 0
private var versionString: String? = null
private var uri: Uri? = null
private var inputStream: InputStream? = null
private var bufferedInputStream: BufferedInputStream? = null
private var status: QueryImageResponseStatusEnum? = null
private var delayedTime: UInt? = null
private var userConsentNeeded: Boolean? = null
fun setOTAFile(
version: Long,
versionString: String,
fileName: String,
uri: Uri?,
userConsentNeeded: Boolean? = null
) {
this.status = QueryImageResponseStatusEnum.UpdateAvailable
this.version = version
this.versionString = versionString
this.fileName = fileName
this.uri = uri
this.delayedTime = null
this.userConsentNeeded = userConsentNeeded
}
fun setOTABusyError(delayedTime: UInt, userConsentNeeded: Boolean? = null) {
this.status = QueryImageResponseStatusEnum.Busy
this.delayedTime = delayedTime
this.userConsentNeeded = userConsentNeeded
}
fun setOTANotAvailableError(userConsentNeeded: Boolean? = null) {
this.status = QueryImageResponseStatusEnum.NotAvailable
this.delayedTime = null
this.userConsentNeeded = userConsentNeeded
}
override fun handleQueryImage(
vendorId: Int,
productId: Int,
softwareVersion: Long,
hardwareVersion: Int?,
location: String?,
requestorCanConsent: Boolean?,
metadataForProvider: ByteArray?
): OTAProviderDelegate.QueryImageResponse? {
Log.d(
TAG,
"handleQueryImage, $vendorId, $productId, $softwareVersion, $hardwareVersion, $location"
)
return when (status) {
QueryImageResponseStatusEnum.UpdateAvailable ->
OTAProviderDelegate.QueryImageResponse(version, versionString, fileName, null)
QueryImageResponseStatusEnum.Busy ->
OTAProviderDelegate.QueryImageResponse(
status,
delayedTime?.toLong() ?: 0,
userConsentNeeded ?: false
)
QueryImageResponseStatusEnum.NotAvailable ->
OTAProviderDelegate.QueryImageResponse(status, userConsentNeeded ?: false)
else -> null
}
}
override fun handleOTAQueryFailure(error: Int) {
Log.d(TAG, "handleOTAQueryFailure, $error")
showMessage("handleOTAQueryFailure : $error")
}
override fun handleApplyUpdateRequest(
nodeId: Long,
newVersion: Long
): OTAProviderDelegate.ApplyUpdateResponse {
Log.d(TAG, "handleApplyUpdateRequest, $nodeId, $newVersion")
return OTAProviderDelegate.ApplyUpdateResponse(
OTAProviderDelegate.ApplyUpdateActionEnum.Proceed,
APPLY_WAITING_TIME
)
}
override fun handleNotifyUpdateApplied(nodeId: Long) {
Log.d(TAG, "handleNotifyUpdateApplied, $nodeId")
showMessage("Finish Firmware Update : $nodeId")
}
override fun handleBDXTransferSessionBegin(
nodeId: Long,
fileDesignator: String?,
offset: Long
) {
Log.d(TAG, "handleBDXTransferSessionBegin, $nodeId, $fileDesignator, $offset")
try {
inputStream = getInputStream(uri)
bufferedInputStream = BufferedInputStream(inputStream)
} catch (e: IOException) {
Log.d(TAG, "exception", e)
inputStream?.close()
bufferedInputStream?.close()
inputStream = null
bufferedInputStream = null
return
}
}
override fun handleBDXTransferSessionEnd(errorCode: Long, nodeId: Long) {
Log.d(TAG, "handleBDXTransferSessionEnd, $errorCode, $nodeId")
inputStream?.close()
bufferedInputStream?.close()
inputStream = null
bufferedInputStream = null
}
override fun handleBDXQuery(
nodeId: Long,
blockSize: Int,
blockIndex: Long,
bytesToSkip: Long
): OTAProviderDelegate.BDXData? {
// This code is just example code. This code doesn't check blockIndex and bytesToSkip
// variable.
Log.d(TAG, "handleBDXQuery, $nodeId, $blockSize, $blockIndex, $bytesToSkip")
showMessage("sending.. $blockIndex")
if (bufferedInputStream == null) {
return OTAProviderDelegate.BDXData(ByteArray(0), true)
}
val packet = ByteArray(blockSize)
val len = bufferedInputStream!!.read(packet)
val sendPacket =
if (len < blockSize) {
packet.copyOf(len)
} else if (len < 0) {
ByteArray(0)
} else {
packet.clone()
}
val isEOF = len < blockSize
return OTAProviderDelegate.BDXData(sendPacket, isEOF)
}
}
inner class ChipControllerCallback : GenericChipDeviceListener() {
override fun onCommissioningComplete(nodeId: Long, errorCode: Long) {
Log.d(TAG, "onCommissioningComplete for nodeId $nodeId: $errorCode")
showMessage("Address update complete for nodeId $nodeId with code $errorCode")
}
override fun onNotifyChipConnectionClosed() {
Log.d(TAG, "onNotifyChipConnectionClosed")
}
override fun onCloseBleComplete() {
Log.d(TAG, "onCloseBleComplete")
}
override fun onError(error: Throwable?) {
Log.d(TAG, "onError: $error")
}
}
private fun showMessage(msg: String) {
requireActivity().runOnUiThread { binding.commandStatusTv.text = msg }
}
companion object {
private const val TAG = "OtaProviderClientFragment"
fun newInstance(): OtaProviderClientFragment = OtaProviderClientFragment()
private const val REQUEST_CODE_OPEN_SAF = 100
private const val APPLY_WAITING_TIME = 10L
private const val OTA_PROVIDER_ENDPOINT_ID = 0
private const val OTA_REQUESTER_ENDPOINT_ID = 0
}
}