diff --git a/src/android/CHIPTool/.idea/misc.xml b/src/android/CHIPTool/.idea/misc.xml deleted file mode 100644 index 37a750962da6f2..00000000000000 --- a/src/android/CHIPTool/.idea/misc.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/ClusterDetailFragment.kt b/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/ClusterDetailFragment.kt index d6c84234ea5e38..72f0c0e33ab319 100644 --- a/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/ClusterDetailFragment.kt +++ b/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/ClusterDetailFragment.kt @@ -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 + 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() + 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 } } @@ -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) { + 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, + 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 { + selectedClusterInfo = clusterMap[clusterName]!! + val commandNameList = constructHint(selectedClusterInfo.commands) + return ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, commandNameList) + } + + private fun constructHint(clusterMap: Map): Array { + 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) + } + } + } } } \ No newline at end of file diff --git a/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/ClusterInteractionFragment.kt b/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/ClusterInteractionFragment.kt index 077e2cbf0aed93..27c1b14f5d43cd 100644 --- a/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/ClusterInteractionFragment.kt +++ b/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/ClusterInteractionFragment.kt @@ -1,7 +1,6 @@ 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 @@ -9,16 +8,17 @@ 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() { @@ -27,7 +27,7 @@ class ClusterInteractionFragment : Fragment() { private lateinit var scope: CoroutineScope private lateinit var addressUpdateFragment: AddressUpdateFragment - private lateinit var clusterMap: Map + private var devicePtr = 0L override fun onCreateView( inflater: LayoutInflater, @@ -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 = ArrayList() // TODO: Dynamically retrieve endpoint information using descriptor cluster // hardcode the endpoint for now @@ -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 { @@ -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)) } } } \ No newline at end of file diff --git a/src/android/CHIPTool/app/src/main/res/layout/cluster_callback_item.xml b/src/android/CHIPTool/app/src/main/res/layout/cluster_callback_item.xml new file mode 100644 index 00000000000000..13844ffaa80d12 --- /dev/null +++ b/src/android/CHIPTool/app/src/main/res/layout/cluster_callback_item.xml @@ -0,0 +1,42 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/android/CHIPTool/app/src/main/res/layout/cluster_detail_fragment.xml b/src/android/CHIPTool/app/src/main/res/layout/cluster_detail_fragment.xml index df7ab6283f1ef4..2b9bd001c3b5a5 100644 --- a/src/android/CHIPTool/app/src/main/res/layout/cluster_detail_fragment.xml +++ b/src/android/CHIPTool/app/src/main/res/layout/cluster_detail_fragment.xml @@ -1,8 +1,48 @@ - + android:layout_height="match_parent" + android:padding="16dp" + android:orientation="vertical"> + - \ No newline at end of file + + +