Skip to content

Commit 85caaa3

Browse files
committedMar 11, 2025
Merge branch 'task/cmd-resp' into 'master'
feature: Add command response feature for nodes See merge request app-frameworks/esp-rainmaker-android!113
2 parents cf0409e + 8e05803 commit 85caaa3

File tree

13 files changed

+667
-12
lines changed

13 files changed

+667
-12
lines changed
 

‎README.md

+8
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,14 @@ This account linking flow enables users to link their Alexa user identity with t
203203
- Link their account without entering Alexa account credentials if already logged into Alexa app. They will have to login to Rainmaker once, when trying to link accounts.
204204
- Link their account from your RainMaker using [Login with Amazon (LWA)](https://developer.amazon.com/docs/login-with-amazon/documentation-overview.html), when the Alexa app isn't installed on their device.
205205

206+
### Command Response
207+
208+
Command Response allows users to send commands to nodes and receive responses back asynchronously. This provides a more robust way of communicating with nodes and also allows nodes to provide access control based on primary/secondary role.
209+
More information about this can be found [here](https://rainmaker.espressif.com/docs/cmd-resp)
210+
211+
This feature is optional and disabled by default.
212+
Add `isCommandResponseSupported=true` in `local.properties` file to enable this feature.
213+
206214
## Matter support
207215

208216
### What is Matter?

‎app/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ android {
122122
buildConfigField "boolean", "isAutomationSupported", localProperties.getProperty("isAutomationSupported", "true")
123123
buildConfigField "boolean", "isOtaSupported", localProperties.getProperty("isOtaSupported", "false")
124124
buildConfigField "boolean", "isMatterSupported", localProperties.getProperty("isMatterSupported", "false")
125+
buildConfigField "boolean", "isCommandResponseSupported", localProperties.getProperty("isCommandResponseSupported", "false")
125126

126127
//---Enable Continuous Updates---//
127128
buildConfigField "boolean", "isContinuousUpdateEnable", localProperties.getProperty("isContinuousUpdateEnable", "true")

‎app/src/main/AndroidManifest.xml

+5
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,11 @@
378378
</intent-filter>
379379
</receiver>
380380

381+
<activity
382+
android:name="com.espressif.ui.activities.CmdRespActivity"
383+
android:screenOrientation="portrait"
384+
android:theme="@style/AppTheme.NoActionBar" />
385+
381386
</application>
382387

383388
<queries>

‎app/src/main/java/com/espressif/AppConstants.kt

+11
Original file line numberDiff line numberDiff line change
@@ -543,5 +543,16 @@ class AppConstants {
543543
EVENT_CTRL_CONFIG_DONE,
544544
EVENT_MATTER_DEVICE_CONNECTIVITY
545545
}
546+
547+
// Command Response constants
548+
const val KEY_RESPONSE_DATA = "response_data"
549+
const val KEY_REQUEST_ID = "request_id"
550+
const val KEY_STATUS_DESCRIPTION = "status_description"
551+
const val KEY_REQUESTS = "requests"
552+
const val KEY_CMD = "cmd"
553+
const val KEY_IS_BASE64 = "is_base64"
554+
const val KEY_TIMEOUT = "timeout"
555+
const val KEY_DATA = "data"
556+
const val URL_USER_NODES_CMD = "/user/nodes/cmd"
546557
}
547558
}

‎app/src/main/java/com/espressif/cloudapi/ApiInterface.java

+9
Original file line numberDiff line numberDiff line change
@@ -346,4 +346,13 @@ Call<ResponseBody> pushFwUpdate(@Url String url, @Header(AppConstants.HEADER_AUT
346346
Call<ResponseBody> convertGroupToFabric(@Url String url, @Header(AppConstants.HEADER_AUTHORIZATION) String token,
347347
@Query(AppConstants.KEY_GROUP_ID) String groupId,
348348
@Body JsonObject body);
349+
350+
@POST
351+
Call<ResponseBody> sendCommandResponse(@Url String url, @Header(AppConstants.HEADER_AUTHORIZATION) String token,
352+
@Body JsonObject requestBody);
353+
354+
@GET
355+
Call<ResponseBody> getCommandResponseStatus(
356+
@Url String url, @Header(AppConstants.HEADER_AUTHORIZATION) String token,
357+
@Query(AppConstants.KEY_REQUEST_ID) String requestId);
349358
}

‎app/src/main/java/com/espressif/cloudapi/ApiManager.java

+92
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,17 @@
7676
import java.util.Map;
7777

7878
import io.reactivex.Observable;
79+
import io.reactivex.android.schedulers.AndroidSchedulers;
7980
import io.reactivex.functions.Consumer;
8081
import io.reactivex.functions.Function;
8182
import io.reactivex.schedulers.Schedulers;
83+
import io.reactivex.observers.DisposableSingleObserver;
84+
import io.reactivex.disposables.Disposable;
8285
import okhttp3.ResponseBody;
8386
import retrofit2.Call;
8487
import retrofit2.Callback;
8588
import retrofit2.Response;
89+
import retrofit2.HttpException;
8690

8791
public class ApiManager {
8892

@@ -4711,4 +4715,92 @@ public void run() {
47114715
public void cancelRequestStatusPollingTask() {
47124716
handler.removeCallbacks(stopRequestStatusPollingTask);
47134717
}
4718+
4719+
public void sendCommandResponse(JsonObject requestBody, ApiResponseListener listener) {
4720+
Log.d(TAG, "Send command response");
4721+
4722+
apiInterface.sendCommandResponse(getBaseUrl() + AppConstants.URL_USER_NODES_CMD,
4723+
accessToken, requestBody).enqueue(new Callback<ResponseBody>() {
4724+
4725+
@Override
4726+
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
4727+
try {
4728+
if (response.isSuccessful()) {
4729+
String jsonResponse = response.body().string();
4730+
Log.d(TAG, "Response : " + jsonResponse);
4731+
JSONObject jsonObject = new JSONObject(jsonResponse);
4732+
String requestId = jsonObject.optString(AppConstants.KEY_REQUEST_ID);
4733+
Bundle data = new Bundle();
4734+
data.putString(AppConstants.KEY_REQUEST_ID, requestId);
4735+
listener.onSuccess(data);
4736+
} else {
4737+
String jsonErrResponse = response.errorBody().string();
4738+
processError(jsonErrResponse, listener, "Failed to send command");
4739+
}
4740+
} catch (Exception e) {
4741+
e.printStackTrace();
4742+
listener.onResponseFailure(e);
4743+
}
4744+
}
4745+
4746+
@Override
4747+
public void onFailure(Call<ResponseBody> call, Throwable t) {
4748+
t.printStackTrace();
4749+
listener.onNetworkFailure(new Exception(t));
4750+
}
4751+
});
4752+
}
4753+
4754+
public void getCommandResponseStatus(String requestId, ApiResponseListener listener) {
4755+
Log.d(TAG, "Get command response status");
4756+
4757+
apiInterface.getCommandResponseStatus(
4758+
getBaseUrl() + AppConstants.URL_USER_NODES_CMD, accessToken,
4759+
requestId).enqueue(new Callback<ResponseBody>() {
4760+
@Override
4761+
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
4762+
try {
4763+
if (response.isSuccessful()) {
4764+
String jsonResponse = response.body().string();
4765+
Log.d(TAG, "Response : " + jsonResponse);
4766+
JSONObject jsonObject = new JSONObject(jsonResponse);
4767+
JSONArray requests = jsonObject.getJSONArray("requests");
4768+
4769+
if (requests.length() > 0) {
4770+
JSONObject request = requests.getJSONObject(0);
4771+
String status = request.optString(AppConstants.KEY_STATUS);
4772+
Bundle data = new Bundle();
4773+
data.putString(AppConstants.KEY_STATUS, status);
4774+
4775+
// Enable button if status is not "requested" or "in_progress"
4776+
boolean enableButton = !status.equals("requested") && !status.equals("in_progress");
4777+
data.putBoolean("enable_button", enableButton);
4778+
4779+
if (request.has(AppConstants.KEY_RESPONSE_DATA)) {
4780+
data.putString(AppConstants.KEY_RESPONSE_DATA, request.getJSONObject(AppConstants.KEY_RESPONSE_DATA).toString());
4781+
}
4782+
if (request.has(AppConstants.KEY_STATUS_DESCRIPTION)) {
4783+
data.putString(AppConstants.KEY_STATUS_DESCRIPTION, request.getString(AppConstants.KEY_STATUS_DESCRIPTION));
4784+
}
4785+
listener.onSuccess(data);
4786+
} else {
4787+
listener.onResponseFailure(new Exception("No request found"));
4788+
}
4789+
} else {
4790+
String jsonErrResponse = response.errorBody().string();
4791+
processError(jsonErrResponse, listener, "Failed to get command status");
4792+
}
4793+
} catch (Exception e) {
4794+
e.printStackTrace();
4795+
listener.onResponseFailure(e);
4796+
}
4797+
}
4798+
4799+
@Override
4800+
public void onFailure(Call<ResponseBody> call, Throwable t) {
4801+
t.printStackTrace();
4802+
listener.onNetworkFailure(new Exception(t));
4803+
}
4804+
});
4805+
}
47144806
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
package com.espressif.ui.activities;
2+
3+
import android.os.Bundle;
4+
import android.os.Handler;
5+
import android.text.TextUtils;
6+
import android.text.TextWatcher;
7+
import android.text.Editable;
8+
import android.util.Log;
9+
import android.view.View;
10+
import android.widget.Button;
11+
import android.widget.EditText;
12+
import android.widget.TextView;
13+
14+
import androidx.appcompat.app.AppCompatActivity;
15+
import androidx.appcompat.widget.Toolbar;
16+
17+
import com.espressif.AppConstants;
18+
import com.espressif.cloudapi.ApiManager;
19+
import com.espressif.cloudapi.ApiResponseListener;
20+
import com.espressif.rainmaker.R;
21+
import com.google.android.material.appbar.MaterialToolbar;
22+
import com.google.android.material.textfield.TextInputEditText;
23+
import com.google.gson.JsonArray;
24+
import com.google.gson.JsonElement;
25+
import com.google.gson.JsonObject;
26+
import com.google.gson.JsonParser;
27+
import com.google.gson.JsonSyntaxException;
28+
29+
import org.json.JSONObject;
30+
import org.json.JSONArray;
31+
32+
public class CmdRespActivity extends AppCompatActivity {
33+
34+
private static final String TAG = CmdRespActivity.class.getSimpleName();
35+
private static final int STATUS_POLLING_INTERVAL = 2000; // 2 seconds
36+
37+
private String nodeId;
38+
private TextInputEditText etId, etPayload, etTimeout;
39+
private TextView tvResponse;
40+
private Button btnSend;
41+
private ApiManager apiManager;
42+
private Handler handler;
43+
private String requestId;
44+
private boolean isPolling = false;
45+
46+
@Override
47+
protected void onCreate(Bundle savedInstanceState) {
48+
super.onCreate(savedInstanceState);
49+
setContentView(R.layout.activity_cmd_resp);
50+
51+
nodeId = getIntent().getStringExtra(AppConstants.KEY_NODE_ID);
52+
initViews();
53+
apiManager = ApiManager.getInstance(this);
54+
handler = new Handler();
55+
}
56+
57+
private void initViews() {
58+
MaterialToolbar toolbar = findViewById(R.id.toolbar);
59+
setSupportActionBar(toolbar);
60+
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
61+
getSupportActionBar().setDisplayShowHomeEnabled(true);
62+
getSupportActionBar().setTitle(R.string.title_activity_cmd_resp);
63+
toolbar.setNavigationIcon(R.drawable.ic_arrow_left);
64+
toolbar.setNavigationOnClickListener(v -> finish());
65+
66+
etId = findViewById(R.id.et_cmd_id);
67+
etPayload = findViewById(R.id.et_cmd_payload);
68+
etTimeout = findViewById(R.id.et_cmd_timeout);
69+
tvResponse = findViewById(R.id.tv_cmd_response);
70+
btnSend = findViewById(R.id.btn_send_cmd);
71+
72+
btnSend.setOnClickListener(v -> sendCommand());
73+
74+
// Add text change listeners to re-enable button
75+
TextWatcher textWatcher = new TextWatcher() {
76+
@Override
77+
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
78+
}
79+
80+
@Override
81+
public void onTextChanged(CharSequence s, int start, int before, int count) {
82+
if (isPolling) {
83+
btnSend.setEnabled(true);
84+
btnSend.setOnClickListener(v -> {
85+
stopPolling();
86+
sendCommand();
87+
});
88+
}
89+
}
90+
91+
@Override
92+
public void afterTextChanged(Editable s) {
93+
}
94+
};
95+
96+
etId.addTextChangedListener(textWatcher);
97+
etPayload.addTextChangedListener(textWatcher);
98+
etTimeout.addTextChangedListener(textWatcher);
99+
}
100+
101+
private void sendCommand() {
102+
String idStr = etId.getText().toString();
103+
String payload = etPayload.getText().toString();
104+
String timeoutStr = etTimeout.getText().toString();
105+
106+
if (TextUtils.isEmpty(idStr)) {
107+
etId.setError(getString(R.string.error_cmd_id_empty));
108+
return;
109+
}
110+
111+
try {
112+
int commandId = Integer.parseInt(idStr);
113+
if (commandId <= 0) {
114+
etId.setError(getString(R.string.error_cmd_id_positive));
115+
return;
116+
}
117+
} catch (NumberFormatException e) {
118+
etId.setError(getString(R.string.error_cmd_id_invalid));
119+
return;
120+
}
121+
122+
if (TextUtils.isEmpty(payload)) {
123+
etPayload.setError(getString(R.string.error_empty_payload));
124+
return;
125+
}
126+
127+
if (TextUtils.isEmpty(timeoutStr)) {
128+
etTimeout.setError(getString(R.string.error_cmd_timeout_empty));
129+
return;
130+
}
131+
132+
int timeout;
133+
try {
134+
timeout = Integer.parseInt(timeoutStr);
135+
if (timeout <= 0) {
136+
etTimeout.setError(getString(R.string.error_cmd_timeout_positive));
137+
return;
138+
}
139+
} catch (NumberFormatException e) {
140+
etTimeout.setError(getString(R.string.error_cmd_timeout_invalid));
141+
return;
142+
}
143+
144+
JsonObject requestBody = new JsonObject();
145+
requestBody.addProperty(AppConstants.KEY_CMD, Integer.parseInt(idStr));
146+
requestBody.addProperty(AppConstants.KEY_IS_BASE64, false);
147+
requestBody.addProperty(AppConstants.KEY_TIMEOUT, timeout);
148+
149+
JsonArray nodeIds = new JsonArray();
150+
nodeIds.add(nodeId);
151+
requestBody.add(AppConstants.KEY_NODE_IDS, nodeIds);
152+
153+
// Try parsing as JSON first
154+
try {
155+
JsonParser parser = new JsonParser();
156+
JsonElement jsonElement;
157+
try {
158+
jsonElement = parser.parse(payload);
159+
} catch (JsonSyntaxException e) {
160+
// Invalid JSON syntax
161+
etPayload.setError(getString(R.string.error_invalid_payload));
162+
tvResponse.setText(getString(R.string.error_invalid_payload));
163+
return;
164+
}
165+
166+
// Valid JSON syntax, but check if it's an object
167+
if (!jsonElement.isJsonObject()) {
168+
etPayload.setError(getString(R.string.error_invalid_json));
169+
tvResponse.setText(getString(R.string.error_invalid_json));
170+
return;
171+
}
172+
requestBody.add(AppConstants.KEY_DATA, jsonElement);
173+
} catch (Exception e) {
174+
// Check if it's base64 encoded
175+
if (isBase64(payload)) {
176+
requestBody.addProperty(AppConstants.KEY_DATA, payload);
177+
requestBody.addProperty(AppConstants.KEY_IS_BASE64, true);
178+
} else {
179+
// Neither valid JSON nor base64
180+
etPayload.setError(getString(R.string.error_invalid_payload));
181+
tvResponse.setText(getString(R.string.error_invalid_payload));
182+
return;
183+
}
184+
}
185+
186+
Log.d(TAG, "Command request body: " + requestBody.toString());
187+
tvResponse.setText("");
188+
btnSend.setEnabled(false);
189+
190+
apiManager.sendCommandResponse(requestBody, new ApiResponseListener() {
191+
@Override
192+
public void onSuccess(Bundle data) {
193+
String reqId = data.getString(AppConstants.KEY_REQUEST_ID);
194+
if (!TextUtils.isEmpty(reqId)) {
195+
requestId = reqId;
196+
String msg = getString(R.string.request_id_received, requestId);
197+
tvResponse.setText(msg);
198+
startPolling();
199+
}
200+
}
201+
202+
@Override
203+
public void onResponseFailure(Exception exception) {
204+
stopPolling();
205+
String error = exception.getMessage();
206+
if (!TextUtils.isEmpty(error)) {
207+
tvResponse.setText(error);
208+
}
209+
}
210+
211+
@Override
212+
public void onNetworkFailure(Exception exception) {
213+
stopPolling();
214+
tvResponse.setText(R.string.error_network);
215+
}
216+
});
217+
}
218+
219+
private boolean isBase64(String str) {
220+
if (TextUtils.isEmpty(str)) {
221+
return false;
222+
}
223+
try {
224+
android.util.Base64.decode(str, android.util.Base64.DEFAULT);
225+
return true;
226+
} catch (IllegalArgumentException e) {
227+
return false;
228+
}
229+
}
230+
231+
private void startPolling() {
232+
if (!isPolling) {
233+
isPolling = true;
234+
btnSend.setEnabled(false);
235+
btnSend.setOnClickListener(v -> stopPolling());
236+
pollStatus();
237+
}
238+
}
239+
240+
private void stopPolling() {
241+
isPolling = false;
242+
handler.removeCallbacksAndMessages(null);
243+
btnSend.setEnabled(true);
244+
btnSend.setOnClickListener(v -> sendCommand());
245+
}
246+
247+
private void pollStatus() {
248+
if (!isPolling) {
249+
return;
250+
}
251+
252+
Log.d(TAG, "Polling status for request: " + requestId);
253+
254+
apiManager.getCommandResponseStatus(requestId, new ApiResponseListener() {
255+
@Override
256+
public void onSuccess(Bundle data) {
257+
Log.d(TAG, "Poll status success: " + data.toString());
258+
runOnUiThread(() -> {
259+
try {
260+
String status = data.getString(AppConstants.KEY_STATUS);
261+
262+
// Display latest status
263+
tvResponse.setText("Status: " + status);
264+
265+
// Enable button if status is not "requested" or "in_progress"
266+
if (!status.equals("requested") && !status.equals("in_progress")) {
267+
btnSend.setEnabled(true);
268+
}
269+
270+
if ("success".equals(status)) {
271+
String responseData = data.getString(AppConstants.KEY_RESPONSE_DATA);
272+
if (!TextUtils.isEmpty(responseData)) {
273+
tvResponse.append("\nResponse: " + responseData);
274+
}
275+
stopPolling();
276+
} else if ("timed_out".equals(status)) {
277+
tvResponse.append("\nReason: Timed Out");
278+
stopPolling();
279+
} else if ("failure".equals(status)) {
280+
String statusDesc = data.getString(AppConstants.KEY_STATUS_DESCRIPTION);
281+
if (!TextUtils.isEmpty(statusDesc)) {
282+
tvResponse.append("\nReason: " + statusDesc);
283+
}
284+
stopPolling();
285+
} else {
286+
handler.postDelayed(() -> pollStatus(), STATUS_POLLING_INTERVAL);
287+
}
288+
} catch (Exception e) {
289+
Log.e(TAG, "Error parsing response", e);
290+
tvResponse.setText("Error parsing response");
291+
stopPolling();
292+
}
293+
});
294+
}
295+
296+
@Override
297+
public void onResponseFailure(Exception exception) {
298+
Log.e(TAG, "Poll status response failure", exception);
299+
runOnUiThread(() -> {
300+
tvResponse.setText(R.string.error_network);
301+
stopPolling();
302+
});
303+
}
304+
305+
@Override
306+
public void onNetworkFailure(Exception exception) {
307+
Log.e(TAG, "Poll status network failure", exception);
308+
runOnUiThread(() -> {
309+
tvResponse.setText(R.string.error_network);
310+
stopPolling();
311+
});
312+
}
313+
});
314+
}
315+
316+
@Override
317+
protected void onDestroy() {
318+
stopPolling();
319+
super.onDestroy();
320+
}
321+
}

‎app/src/main/java/com/espressif/ui/activities/NodeDetailsActivity.java

+26
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@
4949

5050
import java.util.ArrayList;
5151

52+
import org.json.JSONArray;
53+
import org.json.JSONObject;
54+
5255
public class NodeDetailsActivity extends AppCompatActivity {
5356

5457
private RecyclerView nodeInfoRecyclerView;
@@ -159,6 +162,29 @@ private void setNodeInfo() {
159162
nodeInfoValueList.add(node.getNodeType());
160163
}
161164

165+
/* Check for cmd-resp in config */
166+
try {
167+
String configData = node.getConfigData();
168+
if (BuildConfig.isCommandResponseSupported && !TextUtils.isEmpty(configData)) {
169+
JSONObject configJson = new JSONObject(configData);
170+
if (configJson.has("attributes")) {
171+
JSONArray attributes = configJson.getJSONArray("attributes");
172+
for (int i = 0; i < attributes.length(); i++) {
173+
JSONObject attribute = attributes.getJSONObject(i);
174+
if (attribute.has("name") && attribute.has("value") &&
175+
"cmd-resp".equals(attribute.getString("name")) &&
176+
"1".equals(attribute.getString("value"))) {
177+
nodeInfoList.add(getString(R.string.node_cmd_resp));
178+
nodeInfoValueList.add(getString(R.string.btn_send_cmd));
179+
break;
180+
}
181+
}
182+
}
183+
}
184+
} catch (Exception e) {
185+
e.printStackTrace();
186+
}
187+
162188
if (!TextUtils.isEmpty(node.getFwVersion())) {
163189
nodeInfoList.add(getString(R.string.node_fw_version));
164190
nodeInfoValueList.add(node.getFwVersion());

‎app/src/main/java/com/espressif/ui/adapters/NodeDetailsAdapter.java

+26-8
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import com.espressif.cloudapi.CloudException;
4545
import com.espressif.rainmaker.R;
4646
import com.espressif.ui.activities.FwUpdateActivity;
47+
import com.espressif.ui.activities.CmdRespActivity;
4748
import com.espressif.ui.models.EspNode;
4849
import com.espressif.ui.models.Param;
4950
import com.espressif.ui.models.Service;
@@ -84,7 +85,15 @@ public NodeDetailViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
8485
}
8586

8687
@Override
87-
public void onBindViewHolder(@NonNull final NodeDetailViewHolder nodeDetailVh, int position) {
88+
public void onBindViewHolder(@NonNull NodeDetailViewHolder nodeDetailVh, final int position) {
89+
90+
// Default to hide all views
91+
nodeDetailVh.rvSharedUsers.setVisibility(View.GONE);
92+
nodeDetailVh.rlTimezone.setVisibility(View.GONE);
93+
nodeDetailVh.dropDownTimezone.setVisibility(View.GONE);
94+
nodeDetailVh.tvNodeInfoValue.setVisibility(View.GONE);
95+
nodeDetailVh.ivCopy.setVisibility(View.GONE);
96+
nodeDetailVh.ivCopy.setImageResource(R.drawable.ic_copy); // Reset to copy icon by default
8897

8998
// set the data in items
9099
String nodeInfoValue = nodeInfoValueList.get(position);
@@ -99,17 +108,11 @@ public void onBindViewHolder(@NonNull final NodeDetailViewHolder nodeDetailVh, i
99108
}
100109
}
101110

102-
// Default to hide all views
103-
nodeDetailVh.rvSharedUsers.setVisibility(View.GONE);
104-
nodeDetailVh.rlTimezone.setVisibility(View.GONE);
105-
nodeDetailVh.dropDownTimezone.setVisibility(View.GONE);
106-
nodeDetailVh.tvNodeInfoValue.setVisibility(View.GONE);
107-
nodeDetailVh.ivCopy.setVisibility(View.GONE);
108-
109111
// Handle visibility based on the label
110112
if (nodeInfoLabel.equals(context.getString(R.string.node_id))) {
111113

112114
nodeDetailVh.tvNodeInfoValue.setVisibility(View.VISIBLE);
115+
nodeDetailVh.ivCopy.setImageResource(R.drawable.ic_copy);
113116
nodeDetailVh.ivCopy.setVisibility(View.VISIBLE);
114117
nodeDetailVh.tvNodeInfoValue.setText(nodeInfoValue);
115118
nodeDetailVh.ivCopy.setOnClickListener(v -> {
@@ -118,6 +121,7 @@ public void onBindViewHolder(@NonNull final NodeDetailViewHolder nodeDetailVh, i
118121
clipboard.setPrimaryClip(clip);
119122
Toast.makeText(context, R.string.text_copied, Toast.LENGTH_SHORT).show();
120123
});
124+
nodeDetailVh.tvNodeInfoValue.setOnClickListener(null); // Remove any previous click listeners
121125

122126
} else if (nodeInfoLabel.equals(context.getString(R.string.node_fw_update))) {
123127

@@ -309,6 +313,20 @@ public void onNothingSelected(AdapterView<?> parent) {
309313
});
310314
}
311315

316+
} else if (nodeInfoLabel.equals(context.getString(R.string.node_cmd_resp))) {
317+
318+
nodeDetailVh.tvNodeInfoValue.setVisibility(View.VISIBLE);
319+
nodeDetailVh.ivCopy.setImageResource(R.drawable.ic_side_arrow);
320+
nodeDetailVh.ivCopy.setVisibility(View.VISIBLE);
321+
nodeDetailVh.tvNodeInfoValue.setText(nodeInfoValue);
322+
View.OnClickListener cmdRespListener = v -> {
323+
Intent intent = new Intent(context, CmdRespActivity.class);
324+
intent.putExtra(AppConstants.KEY_NODE_ID, node.getNodeId());
325+
context.startActivity(intent);
326+
};
327+
nodeDetailVh.tvNodeInfoValue.setOnClickListener(cmdRespListener);
328+
nodeDetailVh.ivCopy.setOnClickListener(cmdRespListener);
329+
312330
} else if (!TextUtils.isEmpty(nodeInfoValueList.get(position))) {
313331

314332
nodeDetailVh.rvSharedUsers.setVisibility(View.GONE);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:app="http://schemas.android.com/apk/res-auto"
4+
android:layout_width="match_parent"
5+
android:layout_height="match_parent"
6+
android:fitsSystemWindows="true">
7+
8+
<include layout="@layout/toolbar" />
9+
10+
<LinearLayout
11+
android:layout_width="match_parent"
12+
android:layout_height="match_parent"
13+
android:orientation="vertical"
14+
android:padding="16dp"
15+
app:layout_behavior="@string/appbar_scrolling_view_behavior">
16+
17+
<com.google.android.material.textfield.TextInputLayout
18+
android:layout_width="match_parent"
19+
android:layout_height="wrap_content"
20+
android:hint="@string/hint_cmd_id"
21+
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
22+
23+
<com.google.android.material.textfield.TextInputEditText
24+
android:id="@+id/et_cmd_id"
25+
android:layout_width="match_parent"
26+
android:layout_height="wrap_content"
27+
android:inputType="number"
28+
android:maxLines="1" />
29+
30+
</com.google.android.material.textfield.TextInputLayout>
31+
32+
<com.google.android.material.textfield.TextInputLayout
33+
android:layout_width="match_parent"
34+
android:layout_height="wrap_content"
35+
android:layout_marginTop="16dp"
36+
android:hint="@string/hint_cmd_payload"
37+
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
38+
39+
<com.google.android.material.textfield.TextInputEditText
40+
android:id="@+id/et_cmd_payload"
41+
android:layout_width="match_parent"
42+
android:layout_height="wrap_content"
43+
android:inputType="textMultiLine"
44+
android:minLines="3"
45+
android:gravity="top" />
46+
47+
</com.google.android.material.textfield.TextInputLayout>
48+
49+
<com.google.android.material.textfield.TextInputLayout
50+
android:layout_width="match_parent"
51+
android:layout_height="wrap_content"
52+
android:layout_marginTop="@dimen/margin_16"
53+
android:hint="@string/hint_cmd_timeout"
54+
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
55+
56+
<com.google.android.material.textfield.TextInputEditText
57+
android:id="@+id/et_cmd_timeout"
58+
android:layout_width="match_parent"
59+
android:layout_height="wrap_content"
60+
android:inputType="number"
61+
android:maxLines="1"
62+
android:text="60" />
63+
64+
</com.google.android.material.textfield.TextInputLayout>
65+
66+
<Button
67+
android:id="@+id/btn_send_cmd"
68+
android:layout_width="match_parent"
69+
android:layout_height="wrap_content"
70+
android:layout_marginTop="24dp"
71+
android:text="@string/btn_send"
72+
android:textColor="@android:color/white"
73+
style="@style/Widget.MaterialComponents.Button" />
74+
75+
<ScrollView
76+
android:layout_width="match_parent"
77+
android:layout_height="0dp"
78+
android:layout_weight="1"
79+
android:layout_marginTop="16dp">
80+
81+
<TextView
82+
android:id="@+id/tv_cmd_response"
83+
android:layout_width="match_parent"
84+
android:layout_height="wrap_content"
85+
android:textIsSelectable="true"
86+
android:textSize="14sp" />
87+
88+
</ScrollView>
89+
90+
</LinearLayout>
91+
92+
</androidx.coordinatorlayout.widget.CoordinatorLayout>

‎app/src/main/res/values-zh-rCN/strings.xml

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<resources>
22
<string name="action_settings">设置</string>
33
<string name="title_activity_provision_landing">连接设备</string>
4-
<string name="no_internet_note">为了确保您已真正连接到设备,请确保接受系统弹出的网络无互联网连接的提示。</string>
4+
<string name="no_internet_note">为了确保您已真正连接到设备,请确保接受系统弹出的"网络无互联网连接"的提示。</string>
55
<string name="connect_to_device_action">连接</string>
66
<string name="connected_to_device_action">配置网络</string>
77
<string name="ssid_hint_text">网络名称</string>
@@ -156,7 +156,7 @@
156156
<string name="no_automations">未添加自动化</string>
157157
<string name="no_groups">未创建群组</string>
158158
<string name="no_sharing_requests">没有通知</string>
159-
<string name="add_device_txt">您可以通过扫描二维码来配置设备,或者通过点击此屏幕右上角的“+”图标手动进行配置。</string>
159+
<string name="add_device_txt">您可以通过扫描二维码来配置设备,或者通过点击此屏幕右上角的"+"图标手动进行配置。</string>
160160
<string name="signup_confirm_msg">输入验证码。</string>
161161
<string name="node_offline">节点离线</string>
162162
<string name="status_offline">离线</string>
@@ -581,4 +581,28 @@
581581

582582
<string name="status_controller">Controller</string>
583583

584+
<!-- Command Response -->
585+
<string name="node_cmd_resp">命令响应</string>
586+
<string name="btn_send_cmd">发送命令</string>
587+
<string name="title_activity_cmd_resp">命令响应</string>
588+
<string name="hint_cmd_id">ID</string>
589+
<string name="hint_cmd_payload">负载</string>
590+
<string name="btn_send">发送</string>
591+
<string name="error_cmd_id_empty">请输入命令ID</string>
592+
<string name="error_cmd_id_positive">命令ID必须为正数</string>
593+
<string name="error_cmd_id_invalid">无效的命令ID</string>
594+
<string name="error_payload_invalid">无效的JSON负载</string>
595+
<string name="error_network">发生网络错误</string>
596+
<string name="request_id_received">请求ID: %1$s</string>
597+
<string name="cmd_resp_failed">命令响应 %1$s</string>
598+
599+
<string name="error_invalid_json">请输入有效的JSON负载</string>
600+
<string name="error_invalid_payload">请输入有效的JSON或base64编码字符串</string>
601+
<string name="error_empty_payload">请输入负载</string>
602+
603+
<string name="hint_cmd_timeout">超时(秒)</string>
604+
<string name="error_cmd_timeout_empty">请输入超时值</string>
605+
<string name="error_cmd_timeout_positive">超时值必须为正数</string>
606+
<string name="error_cmd_timeout_invalid">无效的超时值</string>
607+
584608
</resources>

‎app/src/main/res/values-zh-rSG/strings.xml

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<resources>
22
<string name="action_settings">设置</string>
33
<string name="title_activity_provision_landing">连接设备</string>
4-
<string name="no_internet_note">为了确保您已真正连接到设备,请确保接受系统弹出的网络无互联网连接的提示。</string>
4+
<string name="no_internet_note">为了确保您已真正连接到设备,请确保接受系统弹出的"网络无互联网连接"的提示。</string>
55
<string name="connect_to_device_action">连接</string>
66
<string name="connected_to_device_action">配置网络</string>
77
<string name="ssid_hint_text">网络名称</string>
@@ -156,7 +156,7 @@
156156
<string name="no_automations">未添加自动化</string>
157157
<string name="no_groups">未创建群组</string>
158158
<string name="no_sharing_requests">没有通知</string>
159-
<string name="add_device_txt">您可以通过扫描二维码来配置设备,或者通过点击此屏幕右上角的“+”图标手动进行配置。</string>
159+
<string name="add_device_txt">您可以通过扫描二维码来配置设备,或者通过点击此屏幕右上角的"+"图标手动进行配置。</string>
160160
<string name="signup_confirm_msg">输入验证码。</string>
161161
<string name="node_offline">节点离线</string>
162162
<string name="status_offline">离线</string>
@@ -581,4 +581,28 @@
581581

582582
<string name="status_controller">Controller</string>
583583

584+
<!-- Command Response -->
585+
<string name="node_cmd_resp">命令响应</string>
586+
<string name="btn_send_cmd">发送命令</string>
587+
<string name="title_activity_cmd_resp">命令响应</string>
588+
<string name="hint_cmd_id">ID</string>
589+
<string name="hint_cmd_payload">负载</string>
590+
<string name="btn_send">发送</string>
591+
<string name="error_cmd_id_empty">请输入命令ID</string>
592+
<string name="error_cmd_id_positive">命令ID必须为正数</string>
593+
<string name="error_cmd_id_invalid">无效的命令ID</string>
594+
<string name="error_payload_invalid">无效的JSON负载</string>
595+
<string name="error_network">发生网络错误</string>
596+
<string name="request_id_received">请求ID: %1$s</string>
597+
<string name="cmd_resp_failed">命令响应 %1$s</string>
598+
599+
<string name="error_invalid_json">请输入有效的JSON负载</string>
600+
<string name="error_invalid_payload">请输入有效的JSON或base64编码字符串</string>
601+
<string name="error_empty_payload">请输入负载</string>
602+
603+
<string name="hint_cmd_timeout">超时(秒)</string>
604+
<string name="error_cmd_timeout_empty">请输入超时值</string>
605+
<string name="error_cmd_timeout_positive">超时值必须为正数</string>
606+
<string name="error_cmd_timeout_invalid">无效的超时值</string>
607+
584608
</resources>

‎app/src/main/res/values/strings.xml

+24
Original file line numberDiff line numberDiff line change
@@ -583,4 +583,28 @@
583583

584584
<string name="status_controller">Controller</string>
585585

586+
<!-- Command Response -->
587+
<string name="node_cmd_resp">Command-Response</string>
588+
<string name="btn_send_cmd">Send Command</string>
589+
<string name="title_activity_cmd_resp">Command-Response</string>
590+
<string name="hint_cmd_id">Id</string>
591+
<string name="hint_cmd_payload">Payload</string>
592+
<string name="btn_send">Send</string>
593+
<string name="error_cmd_id_empty">Please enter command ID</string>
594+
<string name="error_cmd_id_positive">Command ID must be a positive number</string>
595+
<string name="error_cmd_id_invalid">Invalid command ID</string>
596+
<string name="error_payload_invalid">Invalid JSON payload</string>
597+
<string name="error_network">Network error occurred</string>
598+
<string name="request_id_received">Request ID: %1$s</string>
599+
<string name="cmd_resp_failed">Command response %1$s</string>
600+
601+
<string name="error_invalid_json">Please enter a valid JSON payload</string>
602+
<string name="error_invalid_payload">Please enter a valid JSON or base64 encoded string</string>
603+
<string name="error_empty_payload">Please enter payload</string>
604+
605+
<string name="hint_cmd_timeout">Timeout (seconds)</string>
606+
<string name="error_cmd_timeout_empty">Please enter timeout value</string>
607+
<string name="error_cmd_timeout_positive">Timeout must be a positive number</string>
608+
<string name="error_cmd_timeout_invalid">Invalid timeout value</string>
609+
586610
</resources>

0 commit comments

Comments
 (0)
Please sign in to comment.