Skip to content

Commit

Permalink
Cluster command detail page (#11354)
Browse files Browse the repository at this point in the history
* try to find a state where m5stack doesn't bootloop

* add back class description

* select different cluster, command will remove previous displayed parameter

* add responseValueInfo class instead of string split

* remove unused variable

* Restyled by whitespace

* Restyled by google-java-format

* Restyled by gn

* fix parameter response ui alignment issue

* resolve comments

* fix format

* regenerate clusterInfoMapping.java

* resolve comments

* merge with master to get pass the check

Co-authored-by: Restyled.io <[email protected]>
  • Loading branch information
2 people authored and pull[bot] committed Mar 18, 2022
1 parent 25ed589 commit 3a5bbc9
Show file tree
Hide file tree
Showing 12 changed files with 768 additions and 287 deletions.
9 changes: 0 additions & 9 deletions src/android/CHIPTool/.idea/misc.xml

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,37 +1,95 @@
package com.google.chip.chiptool.clusterclient.clusterinteraction

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.AutoCompleteTextView
import android.widget.LinearLayout
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.forEach
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import chip.clusterinfo.ClusterCommandCallback
import chip.clusterinfo.ClusterInfo
import chip.clusterinfo.CommandInfo
import chip.clusterinfo.CommandResponseInfo
import chip.clusterinfo.DelegatedClusterCallback
import chip.devicecontroller.ChipClusters
import chip.devicecontroller.ChipDeviceController
import chip.devicecontroller.ClusterInfoMapping
import com.google.chip.chiptool.ChipClient
import com.google.chip.chiptool.GenericChipDeviceListener
import com.google.chip.chiptool.R
import kotlinx.android.synthetic.main.cluster_callback_item.view.clusterCallbackDataTv
import kotlinx.android.synthetic.main.cluster_callback_item.view.clusterCallbackNameTv
import kotlinx.android.synthetic.main.cluster_callback_item.view.clusterCallbackTypeTv
import kotlinx.android.synthetic.main.cluster_detail_fragment.view.callbackList
import kotlinx.android.synthetic.main.cluster_detail_fragment.view.clusterAutoCompleteTv
import kotlinx.android.synthetic.main.cluster_detail_fragment.view.commandAutoCompleteTv
import kotlinx.android.synthetic.main.cluster_detail_fragment.view.invokeCommand
import kotlinx.android.synthetic.main.cluster_detail_fragment.view.parameterList
import kotlinx.android.synthetic.main.cluster_parameter_item.view.clusterParameterData
import kotlinx.android.synthetic.main.cluster_parameter_item.view.clusterParameterNameTv
import kotlinx.android.synthetic.main.cluster_parameter_item.view.clusterParameterTypeTv
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel

/**
* ClusterDetailFragment allows user to pick cluster, command, specify parameters and see
* the callback result.
*/
class ClusterDetailFragment : Fragment(){
class ClusterDetailFragment : Fragment() {
private val deviceController: ChipDeviceController
get() = ChipClient.getDeviceController(requireContext())

private lateinit var scope: CoroutineScope
private val scope = CoroutineScope(Dispatchers.Main + Job())
private lateinit var clusterMap: Map<String, ClusterInfo>
private lateinit var selectedClusterInfo: ClusterInfo
private lateinit var selectedCluster: ChipClusters.BaseChipCluster
private lateinit var selectedCommandCallback: DelegatedClusterCallback
private lateinit var selectedCommandInfo: CommandInfo
private var devicePtr = 0L
private var endpointId = 0

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
scope = viewLifecycleOwner.lifecycleScope

clusterMap = ClusterInfoMapping().clusterMap
devicePtr = checkNotNull(requireArguments().getLong(DEVICE_PTR_KEY))
endpointId = checkNotNull(requireArguments().getInt(ENDPOINT_ID_KEY))
return inflater.inflate(R.layout.cluster_detail_fragment, container, false).apply {
deviceController.setCompletionListener(GenericChipDeviceListener())
commandAutoCompleteTv.visibility = View.GONE
clusterAutoCompleteSetup(clusterAutoCompleteTv, commandAutoCompleteTv, parameterList)
commandAutoCompleteSetup(commandAutoCompleteTv, inflater, parameterList, callbackList)
invokeCommand.setOnClickListener {
val commandArguments = HashMap<String, Any>()
parameterList.forEach {
val type =
selectedCommandInfo.commandParameters[it.clusterParameterNameTv.text.toString()]!!.type
val data = castStringToType(it.clusterParameterData.text.toString(), type)!!

commandArguments[it.clusterParameterNameTv.text.toString()] = data
}
selectedCommandInfo.getCommandFunction()
.invokeCommand(selectedCluster, selectedCommandCallback, commandArguments)
}
}
}

private fun castStringToType(data: String, type: Class<*>): Any? {
return when (type) {
Int::class.java -> data.toInt()
String::class.java -> data
Boolean::class.java -> data.toBoolean()
else -> null
}
}

Expand All @@ -41,8 +99,119 @@ class ClusterDetailFragment : Fragment(){
}
}

private fun clusterAutoCompleteSetup(
clusterAutoComplete: AutoCompleteTextView,
commandAutoComplete: AutoCompleteTextView,
parameterList: LinearLayout
) {
val clusterNameList = constructHint(clusterMap)
val clusterAdapter =
ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, clusterNameList)
clusterAutoComplete.setAdapter(clusterAdapter)
clusterAutoComplete.setOnItemClickListener { parent, view, position, id ->
commandAutoComplete.visibility = View.VISIBLE
// when new cluster is selected, clear the command text and possible parameterList
commandAutoComplete.setText("", false)
parameterList.removeAllViews()
// populate all the commands that belong to the selected cluster
val selectedCluster: String = clusterAutoComplete.adapter.getItem(position).toString()
val commandAdapter = getCommandOptions(selectedCluster)
commandAutoComplete.setAdapter(commandAdapter)
}
}

private fun commandAutoCompleteSetup(
commandAutoComplete: AutoCompleteTextView,
inflater: LayoutInflater,
parameterList: LinearLayout,
callbackList: LinearLayout
) {
commandAutoComplete.setOnItemClickListener { parent, view, position, id ->
// when new command is selected, clear all the parameterList
parameterList.removeAllViews()
selectedCluster = selectedClusterInfo.createClusterFunction.create(devicePtr, endpointId)
val selectedCommand: String = commandAutoComplete.adapter.getItem(position).toString()
selectedCommandInfo = selectedClusterInfo.commands[selectedCommand]!!
selectedCommandCallback = selectedCommandInfo.commandCallbackSupplier.get()
populateCommandParameter(inflater, parameterList)
selectedCommandCallback.setCallbackDelegate(object : ClusterCommandCallback {
override fun onSuccess(responseValues: Map<CommandResponseInfo, Any>) {
showMessage("Command success")
// Populate UI based on response values. We know the types from CommandInfo.getCommandResponses().
requireActivity().runOnUiThread {
populateCallbackResult(
responseValues,
inflater,
callbackList
)
}
responseValues.forEach { Log.d(TAG, it.toString()) }
}

override fun onFailure(exception: Exception) {
showMessage("Command failed")
Log.e(TAG, exception.toString())
}
})
}
}

private fun populateCommandParameter(inflater: LayoutInflater, parameterList: LinearLayout) {
selectedCommandInfo.commandParameters.forEach { (paramName, paramInfo) ->
val param = inflater.inflate(R.layout.cluster_parameter_item, null, false) as ConstraintLayout
param.clusterParameterNameTv.text = "${paramName}"
param.clusterParameterTypeTv.text = "${paramInfo.type}"
parameterList.addView(param)
}
}

private fun populateCallbackResult(
responseValues: Map<CommandResponseInfo, Any>,
inflater: LayoutInflater,
callbackList: LinearLayout
) {
responseValues.forEach { (variableNameType, response) ->
val callback =
inflater.inflate(R.layout.cluster_callback_item, null, false) as ConstraintLayout
callback.clusterCallbackNameTv.text = variableNameType.name
callback.clusterCallbackDataTv.text = response.toString()
callback.clusterCallbackTypeTv.text = variableNameType.type
callbackList.addView(callback)
}
}

private fun getCommandOptions(
clusterName: String
): ArrayAdapter<String> {
selectedClusterInfo = clusterMap[clusterName]!!
val commandNameList = constructHint(selectedClusterInfo.commands)
return ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, commandNameList)
}

private fun constructHint(clusterMap: Map<String, *>): Array<String> {
return clusterMap.keys.toTypedArray()
}

override fun onStop() {
super.onStop()
scope.cancel()
}

companion object {
private const val TAG = "ClusterDetailFragment"
fun newInstance(): ClusterDetailFragment = ClusterDetailFragment()
private const val ENDPOINT_ID_KEY = "endpoint_id"
private const val DEVICE_PTR_KEY = "device_ptr"

fun newInstance(
deviceId: Long,
endpointId: Int
): ClusterDetailFragment {
return ClusterDetailFragment().apply {
arguments = Bundle(2).apply {
putLong(DEVICE_PTR_KEY, deviceId)
putInt(ENDPOINT_ID_KEY, endpointId)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
package com.google.chip.chiptool.clusterclient.clusterinteraction

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import chip.clusterinfo.ClusterInfo
import chip.devicecontroller.ChipDeviceController
import chip.devicecontroller.ClusterInfoMapping
import com.google.chip.chiptool.ChipClient
import com.google.chip.chiptool.GenericChipDeviceListener
import com.google.chip.chiptool.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import com.google.chip.chiptool.clusterclient.AddressUpdateFragment
import kotlinx.android.synthetic.main.cluster_interaction_fragment.view.endpointList
import kotlinx.android.synthetic.main.cluster_interaction_fragment.view.getEndpointListBtn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

class ClusterInteractionFragment : Fragment() {
Expand All @@ -27,7 +27,7 @@ class ClusterInteractionFragment : Fragment() {

private lateinit var scope: CoroutineScope
private lateinit var addressUpdateFragment: AddressUpdateFragment
private lateinit var clusterMap: Map<String, ClusterInfo>
private var devicePtr = 0L

override fun onCreateView(
inflater: LayoutInflater,
Expand All @@ -37,17 +37,18 @@ class ClusterInteractionFragment : Fragment() {
scope = viewLifecycleOwner.lifecycleScope

return inflater.inflate(R.layout.cluster_interaction_fragment, container, false).apply {
deviceController.setCompletionListener(ChipControllerCallback())
deviceController.setCompletionListener(GenericChipDeviceListener())
endpointList.visibility = View.GONE
getEndpointListBtn.setOnClickListener {
scope.launch {
devicePtr =
ChipClient.getConnectedDevicePointer(requireContext(), addressUpdateFragment.deviceId)
showMessage("Retrieving endpoints")
endpointList.visibility = View.VISIBLE
}
}

addressUpdateFragment =
childFragmentManager.findFragmentById(R.id.addressUpdateFragment) as AddressUpdateFragment
clusterMap = ClusterInfoMapping().clusterMap
var dataList: List<EndpointItem> = ArrayList()
// TODO: Dynamically retrieve endpoint information using descriptor cluster
// hardcode the endpoint for now
Expand All @@ -65,23 +66,9 @@ class ClusterInteractionFragment : Fragment() {
}
}

inner class ChipControllerCallback : GenericChipDeviceListener() {
override fun onConnectDeviceComplete() {}

override fun onCommissioningComplete(nodeId: Long, errorCode: Int) {
}

override fun onNotifyChipConnectionClosed() {
Log.d(TAG, "onNotifyChipConnectionClosed")
}

override fun onCloseBleComplete() {
Log.d(TAG, "onCloseBleComplete")
}

override fun onError(error: Throwable?) {
Log.d(TAG, "onError: $error")
}
override fun onStop() {
super.onStop()
scope.cancel()
}

companion object {
Expand All @@ -104,7 +91,7 @@ class ClusterInteractionFragment : Fragment() {
inner class EndpointListener : EndpointAdapter.OnItemClickListener {
override fun onItemClick(position: Int) {
Toast.makeText(requireContext(), "Item $position clicked", Toast.LENGTH_SHORT).show()
showFragment(ClusterDetailFragment.newInstance())
showFragment(ClusterDetailFragment.newInstance(devicePtr, position))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/clusterCallbackRow"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/clusterCallbackNameTv"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:padding="16dp"
android:minWidth="48dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/clusterCallbackDataTv"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/clusterCallbackDataTv"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="16dp"
android:singleLine="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/clusterCallbackTypeTv"
app:layout_constraintStart_toEndOf="@id/clusterCallbackNameTv"
app:layout_constraintTop_toTopOf="parent" />


<TextView
android:id="@+id/clusterCallbackTypeTv"
android:textStyle="bold"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:padding="16dp"
android:singleLine="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/clusterCallbackDataTv"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Loading

0 comments on commit 3a5bbc9

Please sign in to comment.