From 930381151b4b28604d773c06eaca259d06a5014c Mon Sep 17 00:00:00 2001 From: JasonLiuZhuoCheng Date: Sat, 11 Dec 2021 11:59:33 -0500 Subject: [PATCH] Created history tab to review most-recent-first commands (#12622) * history tab done * polish pr * revert deleted file * further polish pr * remove todo * improve based on comments * resolve static variable conflict * store deviceId instead of devicePtr * Update ClusterDetailFragment.kt --- .../ClusterDetailFragment.kt | 206 +++++++++++++----- .../ClusterInteractionFragment.kt | 33 ++- .../ClusterInteractionHistoryFragment.kt | 65 ++++++ .../ClusterInteractionSettingsFragment.kt | 28 +++ .../clusterinteraction/EndpointAdapter.kt | 2 +- .../clusterinteraction/HistoryCommand.kt | 24 ++ .../HistoryCommandAdapter.kt | 203 +++++++++++++++++ .../app/src/main/res/drawable/ic_history.xml | 10 + .../app/src/main/res/drawable/ic_settings.xml | 10 + .../layout/cluster_interaction_fragment.xml | 10 + .../cluster_interaction_history_fragment.xml | 35 +++ .../cluster_interaction_history_item.xml | 54 +++++ .../cluster_interaction_history_item_info.xml | 38 ++++ .../cluster_interaction_settings_fragment.xml | 13 ++ .../app/src/main/res/layout/endpoint_item.xml | 28 ++- .../cluster_interaction_bottom_navigation.xml | 12 + .../app/src/main/res/values/strings.xml | 1 + 17 files changed, 703 insertions(+), 69 deletions(-) create mode 100644 src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/ClusterInteractionHistoryFragment.kt create mode 100644 src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/ClusterInteractionSettingsFragment.kt create mode 100644 src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/HistoryCommand.kt create mode 100644 src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/HistoryCommandAdapter.kt create mode 100644 src/android/CHIPTool/app/src/main/res/drawable/ic_history.xml create mode 100644 src/android/CHIPTool/app/src/main/res/drawable/ic_settings.xml create mode 100644 src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_history_fragment.xml create mode 100644 src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_history_item.xml create mode 100644 src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_history_item_info.xml create mode 100644 src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_settings_fragment.xml create mode 100644 src/android/CHIPTool/app/src/main/res/menu/cluster_interaction_bottom_navigation.xml 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 7371cce20365b9..87321a93c40211 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 @@ -7,12 +7,14 @@ import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.AutoCompleteTextView +import android.widget.Button import android.widget.LinearLayout import android.widget.Toast import androidx.appcompat.app.AlertDialog 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.InteractionInfo @@ -22,8 +24,11 @@ import chip.devicecontroller.ChipClusters import chip.devicecontroller.ChipDeviceController import chip.devicecontroller.ClusterInfoMapping import com.google.chip.chiptool.ChipClient +import com.google.chip.chiptool.ChipClient.getConnectedDevicePointer import com.google.chip.chiptool.GenericChipDeviceListener import com.google.chip.chiptool.R +import com.google.chip.chiptool.clusterclient.clusterinteraction.ClusterInteractionHistoryFragment.Companion.clusterInteractionHistoryList +import kotlin.properties.Delegates 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 @@ -39,6 +44,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch /** * ClusterDetailFragment allows user to pick cluster, command, specify parameters and see @@ -48,46 +54,134 @@ class ClusterDetailFragment : Fragment() { private val deviceController: ChipDeviceController get() = ChipClient.getDeviceController(requireContext()) - private val scope = CoroutineScope(Dispatchers.Main + Job()) - private lateinit var clusterMap: Map + private lateinit var scope: CoroutineScope + private var clusterMap: Map = ClusterInfoMapping().clusterMap private lateinit var selectedClusterInfo: ClusterInfo private lateinit var selectedCluster: ChipClusters.BaseChipCluster private lateinit var selectedCommandCallback: DelegatedClusterCallback private lateinit var selectedInteractionInfo: InteractionInfo - private var devicePtr = 0L - private var endpointId = 0 + private var devicePtr by Delegates.notNull() + private var deviceId by Delegates.notNull() + private var endpointId by Delegates.notNull() + + // when user opens detail page from home page of cluster interaction, historyCommand will be + // null, and nothing will be autocompleted. If the detail page is opened from history page, + // cluster name, command name and potential parameter list will be filled out based on historyCommand + private var historyCommand: HistoryCommand? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - clusterMap = ClusterInfoMapping().clusterMap - devicePtr = checkNotNull(requireArguments().getLong(DEVICE_PTR_KEY)) + scope = viewLifecycleOwner.lifecycleScope + deviceId = checkNotNull(requireArguments().getLong(DEVICE_ID)) + scope.launch { + devicePtr = getConnectedDevicePointer(requireContext(), deviceId) + } endpointId = checkNotNull(requireArguments().getInt(ENDPOINT_ID_KEY)) + historyCommand = requireArguments().getSerializable(HISTORY_COMMAND) as HistoryCommand? return inflater.inflate(R.layout.cluster_detail_fragment, container, false).apply { deviceController.setCompletionListener(GenericChipDeviceListener()) - commandAutoCompleteTv.visibility = View.GONE - clusterAutoCompleteSetup( + if (historyCommand != null) { + autoCompleteBasedOnHistoryCommand( + historyCommand!!, + clusterAutoCompleteTv, + commandAutoCompleteTv, + parameterList, + inflater, + callbackList + ) + } else { + commandAutoCompleteTv.visibility = View.GONE + clusterAutoCompleteSetup( + clusterAutoCompleteTv, + commandAutoCompleteTv, + parameterList, + callbackList + ) + commandAutoCompleteSetup(commandAutoCompleteTv, inflater, parameterList, callbackList) + } + setInvokeCommandOnClickListener( + invokeCommand, + callbackList, clusterAutoCompleteTv, commandAutoCompleteTv, - parameterList, - callbackList + parameterList ) - commandAutoCompleteSetup(commandAutoCompleteTv, inflater, parameterList, callbackList) - invokeCommand.setOnClickListener { - callbackList.removeAllViews() - val commandArguments = HashMap() - parameterList.forEach { - val type = - selectedInteractionInfo.commandParameters[it.clusterParameterNameTv.text.toString()]!!.type - val data = castStringToType(it.clusterParameterData.text.toString(), type)!! + } + } - commandArguments[it.clusterParameterNameTv.text.toString()] = data - } - selectedInteractionInfo.getCommandFunction() - .invokeCommand(selectedCluster, selectedCommandCallback, commandArguments) + private fun setInvokeCommandOnClickListener( + invokeCommand: Button, + callbackList: LinearLayout, + clusterAutoCompleteTv: AutoCompleteTextView, + commandAutoCompleteTv: AutoCompleteTextView, + parameterList: LinearLayout + ) { + invokeCommand.setOnClickListener { + callbackList.removeAllViews() + val commandArguments = HashMap() + clusterInteractionHistoryList.addFirst( + HistoryCommand( + clusterAutoCompleteTv.text.toString(), + commandAutoCompleteTv.text.toString(), + mutableListOf(), + null, + null, + endpointId, + deviceId + ) + ) + parameterList.forEach { + val parameterName = it.clusterParameterNameTv.text.toString() + val castType = + selectedInteractionInfo.commandParameters[parameterName]!!.type + val data = castStringToType(it.clusterParameterData.text.toString(), castType)!! + commandArguments[it.clusterParameterNameTv.text.toString()] = data + clusterInteractionHistoryList[0].parameterList.add( + HistoryParameterInfo( + parameterName, + data.toString(), + castType + ) + ) } + selectedInteractionInfo.getCommandFunction() + .invokeCommand(selectedCluster, selectedCommandCallback, commandArguments) + } + } + + // Cluster name, command name and parameter list will be autofill based on the given historyCommand + private fun autoCompleteBasedOnHistoryCommand( + historyCommand: HistoryCommand, + clusterAutoComplete: AutoCompleteTextView, + commandAutoComplete: AutoCompleteTextView, + parameterList: LinearLayout, inflater: LayoutInflater, + callbackList: LinearLayout + ) { + clusterAutoComplete.setText(historyCommand.clusterName) + commandAutoComplete.visibility = View.VISIBLE + commandAutoComplete.setText(historyCommand.commandName) + selectedClusterInfo = clusterMap[historyCommand.clusterName]!! + selectedCluster = selectedClusterInfo.createClusterFunction.create(devicePtr, endpointId) + selectedInteractionInfo = selectedClusterInfo.commands[historyCommand.commandName]!! + selectedCommandCallback = selectedInteractionInfo.commandCallbackSupplier.get() + setCallbackDelegate(inflater, callbackList) + historyCommand.parameterList.forEach { + val param = inflater.inflate(R.layout.cluster_parameter_item, null, false) as ConstraintLayout + param.clusterParameterNameTv.text = "${it.parameterName}" + param.clusterParameterTypeTv.text = formatParameterType(it.parameterType) + param.clusterParameterData.setText(it.parameterData) + parameterList.addView(param) + } + } + + private fun formatParameterType(castType: Class<*>): String { + return if (castType == ByteArray::class.java) { + "Byte[]" + } else { + castType.toString() } } @@ -145,26 +239,35 @@ class ClusterDetailFragment : Fragment() { selectedInteractionInfo = selectedClusterInfo.commands[selectedCommand]!! selectedCommandCallback = selectedInteractionInfo.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()) } - } + setCallbackDelegate(inflater, callbackList) + } + } - override fun onFailure(exception: Exception) { - showMessage("Command failed") - Log.e(TAG, exception.toString()) + private fun setCallbackDelegate(inflater: LayoutInflater, callbackList: LinearLayout) { + 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, + ) } - }) - } + clusterInteractionHistoryList[0].responseValue = responseValues + clusterInteractionHistoryList[0].status = "Success" + responseValues.forEach { Log.d(TAG, it.toString()) } + } + + override fun onFailure(exception: Exception) { + showMessage("Command failed") + var errorStatus = exception.toString().split(':') + clusterInteractionHistoryList[0].status = + errorStatus[errorStatus.size - 2] + " " + errorStatus[errorStatus.size - 1] + Log.e(TAG, exception.toString()) + } + }) } private fun populateCommandParameter(inflater: LayoutInflater, parameterList: LinearLayout) { @@ -174,11 +277,7 @@ class ClusterDetailFragment : Fragment() { // byte[].class will be output as class [B, which is not readable, so dynamically change it // to Byte[]. If more custom logic is required, should add a className field in // commandParameterInfo - if (paramInfo.type == ByteArray::class.java) { - param.clusterParameterTypeTv.text = "Byte[]" - } else { - param.clusterParameterTypeTv.text = "${paramInfo.type}" - } + param.clusterParameterTypeTv.text = formatParameterType(paramInfo.type) parameterList.addView(param) } } @@ -241,7 +340,7 @@ class ClusterDetailFragment : Fragment() { } else { it!!.javaClass.toString().split('$').last() } - attributeCallbackItem.clusterCallbackDataTv.text = objectString + attributeCallbackItem.clusterCallbackDataTv.text = callbackClassName attributeCallbackItem.clusterCallbackDataTv.setOnClickListener { AlertDialog.Builder(requireContext()) .setTitle(callbackClassName) @@ -274,19 +373,22 @@ class ClusterDetailFragment : Fragment() { companion object { private const val TAG = "ClusterDetailFragment" - private const val ENDPOINT_ID_KEY = "endpoint_id" - private const val DEVICE_PTR_KEY = "device_ptr" + private const val ENDPOINT_ID_KEY = "endpointId" + private const val HISTORY_COMMAND = "historyCommand" + private const val DEVICE_ID = "deviceId" fun newInstance( deviceId: Long, - endpointId: Int + endpointId: Int, + historyCommand: HistoryCommand? ): ClusterDetailFragment { return ClusterDetailFragment().apply { - arguments = Bundle(2).apply { - putLong(DEVICE_PTR_KEY, deviceId) + arguments = Bundle(4).apply { + putLong(DEVICE_ID, deviceId) + putSerializable(HISTORY_COMMAND, historyCommand) 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 1c3555009644de..950c270b4d43fa 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 @@ -9,12 +9,15 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import chip.devicecontroller.ChipDeviceController +import com.google.android.material.bottomnavigation.BottomNavigationView 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.cancel import com.google.chip.chiptool.clusterclient.AddressUpdateFragment +import kotlin.properties.Delegates +import kotlinx.android.synthetic.main.cluster_interaction_fragment.view.bottomNavigationBar import kotlinx.android.synthetic.main.cluster_interaction_fragment.view.endpointList import kotlinx.android.synthetic.main.cluster_interaction_fragment.view.getEndpointListBtn import kotlinx.coroutines.launch @@ -25,7 +28,7 @@ class ClusterInteractionFragment : Fragment() { private lateinit var scope: CoroutineScope private lateinit var addressUpdateFragment: AddressUpdateFragment - private var devicePtr = 0L + private var devicePtr by Delegates.notNull() override fun onCreateView( inflater: LayoutInflater, @@ -55,6 +58,7 @@ class ClusterInteractionFragment : Fragment() { } endpointList.adapter = EndpointAdapter(dataList, EndpointListener()) endpointList.layoutManager = LinearLayoutManager(requireContext()) + bottomNavigationBar.setOnNavigationItemSelectedListener(bottomNavigationListener) } } @@ -74,22 +78,33 @@ class ClusterInteractionFragment : Fragment() { fun newInstance(): ClusterInteractionFragment = ClusterInteractionFragment() } - private fun showFragment(fragment: Fragment, showOnBack: Boolean = true) { + private fun showFragment(fragment: Fragment) { val fragmentTransaction = requireActivity().supportFragmentManager .beginTransaction() .replace(R.id.fragment_container, fragment, fragment.javaClass.simpleName) + fragmentTransaction.addToBackStack(null) + fragmentTransaction.commit() + } - if (showOnBack) { - fragmentTransaction.addToBackStack(null) + private val bottomNavigationListener = BottomNavigationView.OnNavigationItemSelectedListener { menuItem -> + when (menuItem.itemId) { + R.id.clusterInteractionHistory -> { + val fragment = ClusterInteractionHistoryFragment.newInstance() + showFragment(fragment) + return@OnNavigationItemSelectedListener true + } + R.id.clusterInteractionSettings -> { + val fragment = ClusterInteractionSettingsFragment() + showFragment(fragment) + return@OnNavigationItemSelectedListener true + } } - - fragmentTransaction.commit() + false } inner class EndpointListener : EndpointAdapter.OnItemClickListener { override fun onItemClick(position: Int) { - Toast.makeText(requireContext(), "Item $position clicked", Toast.LENGTH_SHORT).show() - showFragment(ClusterDetailFragment.newInstance(devicePtr, position)) + showFragment(ClusterDetailFragment.newInstance(addressUpdateFragment.deviceId, position, null)) } } -} +} \ No newline at end of file diff --git a/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/ClusterInteractionHistoryFragment.kt b/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/ClusterInteractionHistoryFragment.kt new file mode 100644 index 00000000000000..3eaa874e2c6013 --- /dev/null +++ b/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/ClusterInteractionHistoryFragment.kt @@ -0,0 +1,65 @@ +package com.google.chip.chiptool.clusterclient.clusterinteraction + +import android.os.Bundle +import android.util.Log +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.chip.chiptool.R +import kotlinx.android.synthetic.main.cluster_interaction_history_fragment.view.historyCommandList + +/** + * A simple [Fragment] subclass for the cluster interaction history component + * Use the [ClusterInteractionHistoryFragment.newInstance] factory method to + * create an instance of this fragment. + */ +class ClusterInteractionHistoryFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + Log.d(TAG, clusterInteractionHistoryList.toString()) + return inflater.inflate(R.layout.cluster_interaction_history_fragment, container, false).apply { + historyCommandList.adapter = + HistoryCommandAdapter(clusterInteractionHistoryList, HistoryCommandListener(), inflater) + historyCommandList.layoutManager = LinearLayoutManager(requireContext()) + } + } + + private fun showFragment(fragment: Fragment, showOnBack: Boolean = true) { + val fragmentTransaction = requireActivity().supportFragmentManager + .beginTransaction() + .replace(R.id.fragment_container, fragment, fragment.javaClass.simpleName) + + if (showOnBack) { + fragmentTransaction.addToBackStack(null) + } + + fragmentTransaction.commit() + } + + companion object { + private const val TAG = "ClusterInteractionHistoryFragment" + // The history list is a most-recent-first, therefore adding the most recent executed + // command on the top of the list + val clusterInteractionHistoryList = ArrayDeque() + fun newInstance() = + ClusterInteractionHistoryFragment() + } + + inner class HistoryCommandListener : HistoryCommandAdapter.OnItemClickListener { + override fun onItemClick(position: Int) { + showFragment( + ClusterDetailFragment.newInstance( + clusterInteractionHistoryList[position].deviceId, + clusterInteractionHistoryList[position].endpointId, + clusterInteractionHistoryList[position] + ) + ) + } + } +} \ No newline at end of file diff --git a/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/ClusterInteractionSettingsFragment.kt b/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/ClusterInteractionSettingsFragment.kt new file mode 100644 index 00000000000000..5a535c0025671e --- /dev/null +++ b/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/ClusterInteractionSettingsFragment.kt @@ -0,0 +1,28 @@ +package com.google.chip.chiptool.clusterclient.clusterinteraction + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.google.chip.chiptool.R + +/** + * A simple [Fragment] subclass for cluster interaction settings component. + * Use the [ClusterInteractionSettingsFragment.newInstance] factory method to + * create an instance of this fragment. + */ +class ClusterInteractionSettingsFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.cluster_interaction_settings_fragment, container, false) + } + + companion object { + fun newInstance() = ClusterInteractionSettingsFragment() + } +} \ No newline at end of file diff --git a/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/EndpointAdapter.kt b/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/EndpointAdapter.kt index 104cc1e14e0544..3a339a8a2b29f1 100644 --- a/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/EndpointAdapter.kt +++ b/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/EndpointAdapter.kt @@ -18,7 +18,7 @@ class EndpointAdapter( inner class EndpointViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener { - val endpointId: TextView = itemView.findViewById(R.id.endpointTv) + val endpointId: TextView = itemView.findViewById(R.id.endpointNumberTv) init { itemView.setOnClickListener(this) diff --git a/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/HistoryCommand.kt b/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/HistoryCommand.kt new file mode 100644 index 00000000000000..5105d458ec5512 --- /dev/null +++ b/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/HistoryCommand.kt @@ -0,0 +1,24 @@ +package com.google.chip.chiptool.clusterclient.clusterinteraction + +import chip.clusterinfo.CommandResponseInfo +import java.io.Serializable + +/** + * HistoryCommand stores information about each executed command and display + * necessary information for each item in [ClusterInteractionHistoryFragment] + */ +data class HistoryCommand( + val clusterName: String, + val commandName: String, + val parameterList: MutableList, + var responseValue: Map?, + var status: String?, + var endpointId: Int, + var deviceId: Long, +) : Serializable + +data class HistoryParameterInfo( + val parameterName: String, + val parameterData: String, + val parameterType: Class<*> +) \ No newline at end of file diff --git a/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/HistoryCommandAdapter.kt b/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/HistoryCommandAdapter.kt new file mode 100644 index 00000000000000..bf817b76c9dac6 --- /dev/null +++ b/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/clusterinteraction/HistoryCommandAdapter.kt @@ -0,0 +1,203 @@ +package com.google.chip.chiptool.clusterclient.clusterinteraction + +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.recyclerview.widget.RecyclerView +import chip.clusterinfo.CommandResponseInfo +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_interaction_history_item_info.view.historyClusterNameTv +import kotlinx.android.synthetic.main.cluster_interaction_history_item_info.view.historyCommandNameTv +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 + +/** + * HistoryCommandAdapter implements the historyCommandList(RecycleView) Adapter and associates different + * history command with the same onClick function provided in [ClusterInteractionHistoryFragment.HistoryCommandListener] + */ +class HistoryCommandAdapter( + private val HistoryCommandList: List, + private val listener: ClusterInteractionHistoryFragment.HistoryCommandListener, + private val inflater: LayoutInflater, +) : RecyclerView.Adapter() { + + inner class HistoryCommandViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), + View.OnClickListener { + var historyInfo: LinearLayout = itemView.findViewById(R.id.historyBasicInfo) + var parameterList: LinearLayout = itemView.findViewById(R.id.historyParameterList) + var responseValueList: LinearLayout = itemView.findViewById(R.id.historyResponseValueList) + var statusCode: LinearLayout = itemView.findViewById(R.id.historyItemStatus) + + init { + itemView.setOnClickListener(this) + } + + override fun onClick(endpointItem: View) { + val position = this.adapterPosition + if (position != RecyclerView.NO_POSITION) { + listener.onItemClick(position) + } + } + } + + interface OnItemClickListener { + fun onItemClick(position: Int) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryCommandViewHolder { + val itemView = + LayoutInflater.from(parent.context) + .inflate(R.layout.cluster_interaction_history_item, parent, false) + return HistoryCommandViewHolder(itemView) + } + + override fun getItemCount(): Int { + return HistoryCommandList.size + } + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun getItemViewType(position: Int): Int { + return position + } + + override fun onBindViewHolder( + holder: HistoryCommandAdapter.HistoryCommandViewHolder, + position: Int + ) { + // go through each element and fill the data + // fill out cluster name and command name + clearPreviousReview(holder) + val info = inflater.inflate( + R.layout.cluster_interaction_history_item_info, + null, + false + ) as ConstraintLayout + info.historyClusterNameTv.text = HistoryCommandList[position].clusterName + info.historyCommandNameTv.text = HistoryCommandList[position].commandName + holder.historyInfo.addView(info) + // fill out parameterList + if (HistoryCommandList[position].parameterList.isEmpty()) { + val emptyParameterList = + inflater.inflate(android.R.layout.simple_list_item_1, null, false) as TextView + emptyParameterList.text = "No parameter" + holder.parameterList.addView(emptyParameterList) + } else { + HistoryCommandList[position].parameterList.forEach { + val param = + inflater.inflate(R.layout.cluster_parameter_item, null, false) as ConstraintLayout + param.clusterParameterData.setText(it.parameterData) + param.clusterParameterNameTv.text = it.parameterName + param.clusterParameterTypeTv.text = formatParameterType(it.parameterType) + holder.parameterList.addView(param) + } + } + // fill out responseList + if (HistoryCommandList[position].responseValue == null || HistoryCommandList[position].responseValue!!.isEmpty()) { + val emptyResponseInfo = + inflater.inflate(android.R.layout.simple_list_item_1, null, false) as TextView + emptyResponseInfo.text = "No response" + holder.responseValueList.addView(emptyResponseInfo) + } else { + populateCallbackResult( + HistoryCommandList[position].responseValue!!, + inflater, + holder.responseValueList + ) + } + // fill out status + val statusInfo = inflater.inflate(android.R.layout.simple_list_item_1, null, false) as TextView + statusInfo.text = "Status: " + HistoryCommandList[position].status + holder.statusCode.addView(statusInfo) + } + + private fun populateCallbackResult( + responseValues: Map, + inflater: LayoutInflater, + callbackList: LinearLayout + ) { + responseValues.forEach { (variableNameType, response) -> + if (response is List<*>) { + createListResponseView(response, inflater, callbackList, variableNameType) + } else { + createBasicResponseView(response, inflater, callbackList, variableNameType) + } + } + } + + private fun createBasicResponseView( + response: Any, + inflater: LayoutInflater, + callbackList: LinearLayout, + variableNameType: CommandResponseInfo + ) { + val callbackItem = + inflater.inflate(R.layout.cluster_callback_item, null, false) as ConstraintLayout + callbackItem.clusterCallbackNameTv.text = variableNameType.name + callbackItem.clusterCallbackDataTv.text = if (response.javaClass == ByteArray::class.java) { + (response as ByteArray).decodeToString() + } else { + response.toString() + } + callbackItem.clusterCallbackTypeTv.text = variableNameType.type + callbackList.addView(callbackItem) + } + + private fun createListResponseView( + response: List<*>, + inflater: LayoutInflater, + callbackList: LinearLayout, + variableNameType: CommandResponseInfo + ) { + if (response.isEmpty()) { + val emptyCallback = + inflater.inflate(R.layout.cluster_callback_item, null, false) as ConstraintLayout + emptyCallback.clusterCallbackNameTv.text = "Result is empty" + callbackList.addView(emptyCallback) + } else { + response.forEachIndexed { index, it -> + val attributeCallbackItem = + inflater.inflate(R.layout.cluster_callback_item, null, false) as ConstraintLayout + attributeCallbackItem.clusterCallbackNameTv.text = variableNameType.name + "[$index]" + val objectString = if (it!!.javaClass == ByteArray::class.java) { + (it as ByteArray).contentToString() + } else { + it.toString() + } + var callbackClassName = if (it!!.javaClass == ByteArray::class.java) { + "Byte[]" + } else { + it!!.javaClass.toString().split('$').last() + } + attributeCallbackItem.clusterCallbackDataTv.text = objectString + attributeCallbackItem.clusterCallbackTypeTv.text = "List<$callbackClassName>" + callbackList.addView(attributeCallbackItem) + } + } + } + + private fun formatParameterType(castType: Class<*>): String { + return if (castType == ByteArray::class.java) { + "Byte[]" + } else { + castType.toString() + } + } + + private fun clearPreviousReview(holder: HistoryCommandAdapter.HistoryCommandViewHolder) { + holder.historyInfo.removeAllViews() + holder.parameterList.removeAllViews() + holder.responseValueList.removeAllViews() + holder.statusCode.removeAllViews() + } +} \ No newline at end of file diff --git a/src/android/CHIPTool/app/src/main/res/drawable/ic_history.xml b/src/android/CHIPTool/app/src/main/res/drawable/ic_history.xml new file mode 100644 index 00000000000000..b4c803f15eeaf1 --- /dev/null +++ b/src/android/CHIPTool/app/src/main/res/drawable/ic_history.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/src/android/CHIPTool/app/src/main/res/drawable/ic_settings.xml b/src/android/CHIPTool/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 00000000000000..2f249c9e8c6276 --- /dev/null +++ b/src/android/CHIPTool/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_fragment.xml b/src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_fragment.xml index 2b50a2b988e526..8cb9ccfd954e7e 100644 --- a/src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_fragment.xml +++ b/src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_fragment.xml @@ -30,5 +30,15 @@ android:clipToPadding="false" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/getEndpointListBtn" /> + \ No newline at end of file diff --git a/src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_history_fragment.xml b/src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_history_fragment.xml new file mode 100644 index 00000000000000..df560633d8a7c5 --- /dev/null +++ b/src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_history_fragment.xml @@ -0,0 +1,35 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_history_item.xml b/src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_history_item.xml new file mode 100644 index 00000000000000..0368d7b49dd4f9 --- /dev/null +++ b/src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_history_item.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_history_item_info.xml b/src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_history_item_info.xml new file mode 100644 index 00000000000000..4087b580cda4d6 --- /dev/null +++ b/src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_history_item_info.xml @@ -0,0 +1,38 @@ + + + + + + + + /> + + \ No newline at end of file diff --git a/src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_settings_fragment.xml b/src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_settings_fragment.xml new file mode 100644 index 00000000000000..49312d65d0c537 --- /dev/null +++ b/src/android/CHIPTool/app/src/main/res/layout/cluster_interaction_settings_fragment.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/src/android/CHIPTool/app/src/main/res/layout/endpoint_item.xml b/src/android/CHIPTool/app/src/main/res/layout/endpoint_item.xml index ac93ca48438924..86ccc79dea7967 100644 --- a/src/android/CHIPTool/app/src/main/res/layout/endpoint_item.xml +++ b/src/android/CHIPTool/app/src/main/res/layout/endpoint_item.xml @@ -9,21 +9,35 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:padding="8dp" - android:layout_margin="4dp"> + android:layout_margin="4dp" + android:background="@color/cardview_shadow_start_color"> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintWidth_percent="0.25" /> + + diff --git a/src/android/CHIPTool/app/src/main/res/menu/cluster_interaction_bottom_navigation.xml b/src/android/CHIPTool/app/src/main/res/menu/cluster_interaction_bottom_navigation.xml new file mode 100644 index 00000000000000..c6b2a95369eead --- /dev/null +++ b/src/android/CHIPTool/app/src/main/res/menu/cluster_interaction_bottom_navigation.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/src/android/CHIPTool/app/src/main/res/values/strings.xml b/src/android/CHIPTool/app/src/main/res/values/strings.xml index 093940527c9f55..2ba82c650ca3e4 100644 --- a/src/android/CHIPTool/app/src/main/res/values/strings.xml +++ b/src/android/CHIPTool/app/src/main/res/values/strings.xml @@ -151,4 +151,5 @@ Invoke Select a command Select a cluster + Endpoint: