blob: 43eeb2b892c51c7e72a86a99b299b7f271facbcf [file] [log] [blame]
package com.google.chip.chiptool.clusterclient
import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.os.Looper
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.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import chip.devicecontroller.ChipClusterException
import chip.devicecontroller.ChipClusters
import chip.devicecontroller.ChipDeviceController
import chip.devicecontroller.ICDClientInfo
import com.google.chip.chiptool.ChipClient
import com.google.chip.chiptool.databinding.AddressUpdateFragmentBinding
import com.google.chip.chiptool.util.DeviceIdUtil
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
/** Fragment for updating the address of a device given its fabric and node ID. */
class AddressUpdateFragment : ICDCheckInCallback, Fragment() {
private val deviceController: ChipDeviceController
get() = ChipClient.getDeviceController(requireContext())
val deviceId: Long
get() = binding.deviceIdEd.text.toString().toULong(16).toLong()
var endpointId: Int
get() = binding.epIdEd.text.toString().toInt()
set(value) {
binding.epIdEd.setText(value.toString())
}
private var _binding: AddressUpdateFragmentBinding? = null
private val binding
get() = _binding!!
private lateinit var scope: CoroutineScope
private var icdDeviceId: Long = 0L
private var icdTotalRemainStayActiveTimeMs = 0L
private var icdDeviceRemainStayActiveTimeMs = 0L
private var isSendingStayActiveCommand = false
private val icdRequestActiveDurationMs: Long
get() = binding.icdActiveDurationEd.text.toString().toLong() * 1000
private val handler = Handler(Looper.getMainLooper())
private var externalICDCheckInMessageCallback: ICDCheckInMessageCallback? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = AddressUpdateFragmentBinding.inflate(inflater, container, false)
scope = viewLifecycleOwner.lifecycleScope
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ChipClient.setICDCheckInCallback(this)
val compressedFabricId = deviceController.compressedFabricId
binding.fabricIdEd.setText(compressedFabricId.toULong().toString(16).padStart(16, '0'))
binding.deviceIdEd.setText(DeviceIdUtil.getLastDeviceId(requireContext()).toString(16))
binding.epIdEd.setText(endpointId.toString())
binding.icdActiveDurationEd.setText((ICD_STAY_ACTIVE_DURATION / 1000).toString())
binding.icdInteractionSwitch.setOnClickListener {
val isChecked = binding.icdInteractionSwitch.isChecked
if (updateUIForICDInteractionSwitch(isChecked)) {
icdInteractionSwitchClick(isChecked)
}
}
updateDeviceIdSpinner()
}
fun updateDeviceIdSpinner() {
val deviceIdList = DeviceIdUtil.getCommissionedNodeId(requireContext())
binding.deviceIdSpinner.adapter =
ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, deviceIdList)
binding.deviceIdSpinner.onItemSelectedListener =
object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
binding.deviceIdEd.setText(deviceIdList[position].toULong(16).toString())
}
override fun onNothingSelected(parent: AdapterView<*>?) {
Log.d(TAG, "onNothingSelected")
}
}
}
private fun icdInteractionSwitchClick(isEnabled: Boolean) {
if (isEnabled) {
icdDeviceId = deviceId
} else {
icdDeviceId = 0
if (icdDeviceRemainStayActiveTimeMs != 0L || icdTotalRemainStayActiveTimeMs != 0L) {
scope.launch {
icdDeviceRemainStayActiveTimeMs = sendStayActive(0L)
icdTotalRemainStayActiveTimeMs = icdDeviceRemainStayActiveTimeMs
}
}
}
}
private suspend fun sendStayActive(duration: Long): Long {
isSendingStayActiveCommand = true
val devicePtr =
try {
ChipClient.getConnectedDevicePointer(requireContext(), deviceId)
} catch (e: IllegalStateException) {
Log.d(TAG, "getConnectedDevicePointer exception", e)
showToastMessage("Get DevicePointer fail!")
throw e
}
val cluster = ChipClusters.IcdManagementCluster(devicePtr, 0)
val retDuration = suspendCoroutine { cont ->
cluster.stayActiveRequest(
object : ChipClusters.IcdManagementCluster.StayActiveResponseCallback {
override fun onError(error: Exception) {
Log.d(TAG, "onError", error)
cont.resume(0L)
}
override fun onSuccess(promisedActiveDuration: Long) {
cont.resume(promisedActiveDuration)
}
},
duration
)
}
isSendingStayActiveCommand = false
return retDuration
}
private fun updateUIForICDInteractionSwitch(isEnabled: Boolean): Boolean {
val isICD = isICDDevice()
if (isEnabled && !isICD) {
binding.icdInteractionSwitch.isChecked = false
return false
}
return true
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
suspend fun getDevicePointer(context: Context): Long {
return if (isGroupId()) {
deviceController.getGroupDevicePointer(getGroupId().toInt())
} else {
ChipClient.getConnectedDevicePointer(context, getNodeId().toLong())
}
}
private fun showToastMessage(msg: String) {
requireActivity().runOnUiThread {
Toast.makeText(requireActivity(), msg, Toast.LENGTH_SHORT).show()
}
}
fun isGroupId(): Boolean {
return isGroupNodeId(getNodeId())
}
fun getGroupId(): UInt {
return getGroupIdFromNodeId(getNodeId())
}
fun getNodeId(): ULong {
return binding.deviceIdEd.text.toString().toULong(16)
}
fun isICDDevice(): Boolean {
return deviceController.icdClientInfo.firstOrNull { info -> info.peerNodeId == deviceId } !=
null
}
override fun notifyCheckInMessage(info: ICDClientInfo) {
externalICDCheckInMessageCallback?.notifyCheckInMessage()
if (info.peerNodeId != icdDeviceId) {
return
}
scope.launch {
try {
icdDeviceRemainStayActiveTimeMs = sendStayActive(icdRequestActiveDurationMs)
icdTotalRemainStayActiveTimeMs = icdRequestActiveDurationMs
turnOnActiveMode()
} catch (e: IllegalStateException) {
Log.d(TAG, "IlligalStateException", e)
} catch (e: ChipClusterException) {
Log.d(TAG, "ChipClusterException", e)
}
}
}
fun setNotifyCheckInMessageCallback(callback: ICDCheckInMessageCallback?) {
externalICDCheckInMessageCallback = callback
}
interface ICDCheckInMessageCallback {
fun notifyCheckInMessage()
}
private fun turnOnActiveMode() {
requireActivity().runOnUiThread {
binding.icdProgressBar.max = (icdTotalRemainStayActiveTimeMs / 1000).toInt()
binding.icdProgressBar.progress = (icdTotalRemainStayActiveTimeMs / 1000).toInt()
}
val runnable =
object : Runnable {
override fun run() {
if (icdTotalRemainStayActiveTimeMs >= ICD_PROGRESS_STEP) {
icdDeviceRemainStayActiveTimeMs -= ICD_PROGRESS_STEP
icdTotalRemainStayActiveTimeMs -= ICD_PROGRESS_STEP
handler.postDelayed(this, ICD_PROGRESS_STEP)
requireActivity().runOnUiThread {
binding.icdProgressBar.progress = (icdTotalRemainStayActiveTimeMs / 1000).toInt()
}
if (
!isSendingStayActiveCommand &&
(ICD_RESEND_STAY_ACTIVE_TIME > icdDeviceRemainStayActiveTimeMs) &&
(ICD_RESEND_STAY_ACTIVE_TIME < icdTotalRemainStayActiveTimeMs)
)
scope.launch {
icdDeviceRemainStayActiveTimeMs = sendStayActive(icdTotalRemainStayActiveTimeMs)
}
} else {
requireActivity().runOnUiThread { binding.icdProgressBar.progress = 0 }
}
}
}
handler.post(runnable)
}
companion object {
private const val TAG = "AddressUpdateFragment"
// Refer from NodeId.h (src/lib/core/NodeId.h)
private const val MIN_GROUP_NODE_ID = 0xFFFF_FFFF_FFFF_0000UL
private const val MASK_GROUP_ID = 0x0000_0000_0000_FFFFUL
private const val ICD_STAY_ACTIVE_DURATION = 30000L // 30 secs.
private const val ICD_PROGRESS_STEP = 1000L // 1 sec.
private const val ICD_RESEND_STAY_ACTIVE_TIME = 2000L // 2 secs.
fun isGroupNodeId(nodeId: ULong): Boolean {
return nodeId >= MIN_GROUP_NODE_ID
}
fun getNodeIdFromGroupId(groupId: UInt): ULong {
return groupId.toULong() or MIN_GROUP_NODE_ID
}
fun getGroupIdFromNodeId(nodeId: ULong): UInt {
return (nodeId and MASK_GROUP_ID).toUInt()
}
}
}
interface ICDCheckInCallback {
fun notifyCheckInMessage(info: ICDClientInfo)
}