Skip to content

Commit

Permalink
[Android] Add Stay Active Button in Chiptool (project-chip#32983)
Browse files Browse the repository at this point in the history
* Add send Stay Active in Android Chiptool

* Add kotlin static exception
  • Loading branch information
joonhaengHeo authored Apr 15, 2024
1 parent 8d47f40 commit 25d4643
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import chip.platform.NsdManagerServiceResolver
import chip.platform.PreferencesConfigurationManager
import chip.platform.PreferencesKeyValueStoreManager
import com.google.chip.chiptool.attestation.ExampleAttestationTrustStoreDelegate
import com.google.chip.chiptool.clusterclient.ICDCheckInCallback
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.suspendCancellableCoroutine
Expand All @@ -45,6 +46,8 @@ object ChipClient {
/* 0xFFF4 is a test vendor ID, replace with your assigned company ID */
const val VENDOR_ID = 0xFFF4

private var icdCheckInCallback: ICDCheckInCallback? = null

fun getDeviceController(context: Context): ChipDeviceController {
getAndroidChipPlatform(context)

Expand All @@ -67,6 +70,7 @@ object ChipClient {
object : ICDCheckInDelegate {
override fun onCheckInComplete(info: ICDClientInfo) {
Log.d(TAG, "onCheckInComplete : $info")
icdCheckInCallback?.notifyCheckInMessage(info)
}

override fun onKeyRefreshNeeded(info: ICDClientInfo): ByteArray? {
Expand Down Expand Up @@ -106,6 +110,10 @@ object ChipClient {
return androidPlatform
}

fun setICDCheckInCallback(callback: ICDCheckInCallback) {
icdCheckInCallback = callback
}

/**
* Wrapper around [ChipDeviceController.getConnectedDevicePointer] to return the value directly.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,37 @@ 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.resumeWithException
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 : Fragment() {
class AddressUpdateFragment : ICDCheckInCallback, Fragment() {
private val deviceController: ChipDeviceController
get() = ChipClient.getDeviceController(requireContext())

val deviceId: Long
get() = binding.deviceIdEd.text.toString().toULong().toLong()
get() = binding.deviceIdEd.text.toString().toULong(16).toLong()

var endpointId: Int
get() = binding.epIdEd.text.toString().toInt()
Expand All @@ -32,22 +44,44 @@ class AddressUpdateFragment : Fragment() {
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())

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().padStart(16, '0'))
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()
}
Expand All @@ -68,6 +102,61 @@ class AddressUpdateFragment : Fragment() {
}
}

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 duration = suspendCoroutine { cont ->
cluster.stayActiveRequest(
object : ChipClusters.IcdManagementCluster.StayActiveResponseCallback {
override fun onError(error: Exception) {
cont.resumeWithException(error)
}

override fun onSuccess(promisedActiveDuration: Long) {
cont.resume(promisedActiveDuration)
}
},
duration
)
}
isSendingStayActiveCommand = false
return duration
}

private fun updateUIForICDInteractionSwitch(isEnabled: Boolean): Boolean {
val isICD =
deviceController.icdClientInfo.firstOrNull { info -> info.peerNodeId == deviceId } != null
if (isEnabled && !isICD) {
binding.icdInteractionSwitch.isChecked = false
return false
}

return true
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
Expand All @@ -81,6 +170,12 @@ class AddressUpdateFragment : Fragment() {
}
}

private fun showToastMessage(msg: String) {
requireActivity().runOnUiThread {
Toast.makeText(requireActivity(), msg, Toast.LENGTH_SHORT).show()
}
}

fun isGroupId(): Boolean {
return isGroupNodeId(getNodeId())
}
Expand All @@ -90,7 +185,58 @@ class AddressUpdateFragment : Fragment() {
}

fun getNodeId(): ULong {
return binding.deviceIdEd.text.toString().toULong()
return binding.deviceIdEd.text.toString().toULong(16)
}

override fun notifyCheckInMessage(info: ICDClientInfo) {
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)
}
}
}

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 {
Expand All @@ -99,6 +245,10 @@ class AddressUpdateFragment : Fragment() {
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
}
Expand All @@ -112,3 +262,7 @@ class AddressUpdateFragment : Fragment() {
}
}
}

interface ICDCheckInCallback {
fun notifyCheckInMessage(info: ICDClientInfo)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,24 @@
android:id="@+id/fabricIdEd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textSize="16sp"
android:hint="@string/enter_fabric_id_hint_text"
android:inputType="number"
android:digits="0123456789ABCDEF"
android:inputType="textCapCharacters"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toLeftOf="@+id/deviceIdEd"/>
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintRight_toLeftOf="@id/deviceIdEd"/>

<EditText
android:id="@+id/deviceIdEd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="number"
android:textSize="20sp"
android:digits="0123456789ABCDEF"
android:inputType="textCapCharacters"
android:textSize="16sp"
android:hint="@string/enter_device_id_hint_text"
app:layout_constraintStart_toEndOf="@+id/fabricIdEd"
app:layout_constraintStart_toEndOf="@id/fabricIdEd"
app:layout_constraintEnd_toStartOf="@id/epIdEd"
app:layout_constraintTop_toTopOf="parent" />

<EditText
Expand All @@ -31,25 +35,73 @@
android:layout_height="wrap_content"
android:hint="@string/enter_endpoint_id_hint_text"
android:inputType="number"
android:textSize="20sp"
android:textSize="16sp"
android:text ="1"
app:layout_constraintStart_toEndOf="@+id/deviceIdEd"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="fabricIdEd,deviceIdEd,epIdEd" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="fabricIdEd,deviceIdEd,epIdEd" />

<Switch
android:id="@+id/icdInteractionSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
android:textSize="16sp"
android:text="@string/icd_interaction_text"
app:layout_constraintTop_toBottomOf="@id/fabricIdEd"
app:layout_constraintStart_toStartOf="parent"
tools:ignore="UseSwitchCompatOrMaterialXml" />

<EditText
android:id="@+id/icdActiveDurationEd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="@string/icd_active_duration_hint_text"
android:inputType="number"
android:textSize="16sp"
android:text ="30"
app:layout_constraintTop_toBottomOf="@id/fabricIdEd"
app:layout_constraintStart_toEndOf="@id/icdInteractionSwitch"
app:layout_constraintEnd_toStartOf="@id/icdProgressBar" />

<ProgressBar
android:id="@+id/icdProgressBar"
android:layout_width="100dp"
android:layout_height="30dp"
style="?android:attr/progressBarStyleHorizontal"
android:layout_marginStart="6dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
android:progress="0"
android:max="100"
app:layout_constraintTop_toBottomOf="@id/fabricIdEd"
app:layout_constraintStart_toEndOf="@id/icdActiveDurationEd"
app:layout_constraintEnd_toEndOf="parent"/>

<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="icdInteractionSwitch,icdActiveDurationEd,icdProgressBar" />

<TextView
android:id="@+id/deviceIdTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:layout_marginTop="6dp"
android:textSize="16sp"
android:text = "@string/commissioned_device_id_text"
app:layout_constraintTop_toBottomOf="@id/fabricIdEd"
app:layout_constraintTop_toBottomOf="@id/icdInteractionSwitch"
/>

<Spinner
Expand All @@ -60,4 +112,6 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/deviceIdTv" />



</androidx.constraintlayout.widget.ConstraintLayout>
Loading

0 comments on commit 25d4643

Please sign in to comment.