[Android] Add initial batch command support (#32326)
diff --git a/.github/workflows/java-tests.yaml b/.github/workflows/java-tests.yaml
index f19d451..3aa6935 100644
--- a/.github/workflows/java-tests.yaml
+++ b/.github/workflows/java-tests.yaml
@@ -132,6 +132,17 @@
--tool-cluster "im" \
--tool-args "onnetwork-long-im-invoke --nodeid 1 --setup-pin-code 20202021 --discriminator 3840 -t 1000" \
--factoryreset \
+ '
+ - name: Run IM Batch Invoke Test
+ run: |
+ scripts/run_in_python_env.sh out/venv \
+ './scripts/tests/run_java_test.py \
+ --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app \
+ --app-args "--discriminator 3840 --interface-id -1" \
+ --tool-path out/linux-x64-java-matter-controller \
+ --tool-cluster "im" \
+ --tool-args "onnetwork-long-im-batch-invoke --nodeid 1 --setup-pin-code 20202021 --discriminator 3840 -t 1000" \
+ --factoryreset \
'
- name: Run IM Read Test
run: |
diff --git a/examples/java-matter-controller/BUILD.gn b/examples/java-matter-controller/BUILD.gn
index 34de1ed..76acd19 100644
--- a/examples/java-matter-controller/BUILD.gn
+++ b/examples/java-matter-controller/BUILD.gn
@@ -57,6 +57,7 @@
"java/src/com/matter/controller/commands/pairing/PairOnNetworkFabricCommand.kt",
"java/src/com/matter/controller/commands/pairing/PairOnNetworkInstanceNameCommand.kt",
"java/src/com/matter/controller/commands/pairing/PairOnNetworkLongCommand.kt",
+ "java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImExtendableInvokeCommand.kt",
"java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImInvokeCommand.kt",
"java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImReadCommand.kt",
"java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImSubscribeCommand.kt",
diff --git a/examples/java-matter-controller/java/src/com/matter/controller/Main.kt b/examples/java-matter-controller/java/src/com/matter/controller/Main.kt
index d0e2ef8..a1a66a8 100644
--- a/examples/java-matter-controller/java/src/com/matter/controller/Main.kt
+++ b/examples/java-matter-controller/java/src/com/matter/controller/Main.kt
@@ -67,6 +67,7 @@
PairOnNetworkLongImSubscribeCommand(controller, credentialsIssuer),
PairOnNetworkLongImWriteCommand(controller, credentialsIssuer),
PairOnNetworkLongImInvokeCommand(controller, credentialsIssuer),
+ PairOnNetworkLongImExtendableInvokeCommand(controller, credentialsIssuer),
)
}
diff --git a/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImExtendableInvokeCommand.kt b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImExtendableInvokeCommand.kt
new file mode 100644
index 0000000..d80e71a
--- /dev/null
+++ b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImExtendableInvokeCommand.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (c) 2024 Project CHIP Authors
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.matter.controller.commands.pairing
+
+import chip.devicecontroller.ChipDeviceController
+import chip.devicecontroller.ExtendableInvokeCallback
+import chip.devicecontroller.GetConnectedDeviceCallbackJni.GetConnectedDeviceCallback
+import chip.devicecontroller.model.InvokeElement
+import chip.devicecontroller.model.InvokeResponseData
+import chip.devicecontroller.model.NoInvokeResponseData
+import chip.devicecontroller.model.Status
+import com.matter.controller.commands.common.CredentialsIssuer
+import java.util.logging.Level
+import java.util.logging.Logger
+import kotlin.UShort
+import matter.tlv.AnonymousTag
+import matter.tlv.ContextSpecificTag
+import matter.tlv.TlvWriter
+
+class PairOnNetworkLongImExtendableInvokeCommand(
+ controller: ChipDeviceController,
+ credsIssue: CredentialsIssuer?
+) :
+ PairingCommand(
+ controller,
+ "onnetwork-long-im-batch-invoke",
+ credsIssue,
+ PairingModeType.ON_NETWORK,
+ PairingNetworkType.NONE,
+ DiscoveryFilterType.LONG_DISCRIMINATOR
+ ) {
+ private var devicePointer: Long = 0
+
+ private fun setDevicePointer(devicePointer: Long) {
+ this.devicePointer = devicePointer
+ }
+
+ private inner class InternalInvokeCallback : ExtendableInvokeCallback {
+ private var responseCount = 0
+
+ override fun onError(e: Exception) {
+ logger.log(Level.INFO, "Batch Invoke receive onError" + e.message)
+ setFailure("invoke failure")
+ }
+
+ override fun onResponse(invokeResponseData: InvokeResponseData) {
+ logger.log(Level.INFO, "Batch Invoke receive OnResponse on $invokeResponseData")
+ val clusterId = invokeResponseData.getClusterId().getId()
+ val commandId = invokeResponseData.getCommandId().getId()
+ val tlvData = invokeResponseData.getTlvByteArray()
+ val jsonData = invokeResponseData.getJsonString()
+ val status = invokeResponseData.getStatus()
+
+ if (clusterId == CLUSTER_ID_IDENTIFY && commandId == IDENTIFY_COMMAND) {
+ if (tlvData != null || jsonData != null) {
+ setFailure("invoke failure with problematic payload")
+ }
+ if (
+ status != null && status.status != Status.Code.Success && status.clusterStatus.isPresent()
+ ) {
+ setFailure("invoke failure with incorrect status")
+ }
+ }
+
+ if (clusterId == CLUSTER_ID_TEST && commandId == TEST_ADD_ARGUMENT_RSP_COMMAND) {
+ if (tlvData == null || jsonData == null) {
+ setFailure("invoke failure with problematic payload")
+ }
+
+ if (!jsonData.equals("""{"0:UINT":2}""")) {
+ setFailure("invoke failure with problematic json")
+ }
+
+ if (status != null) {
+ setFailure("invoke failure with incorrect status")
+ }
+ }
+ responseCount++
+ }
+
+ override fun onNoResponse(noInvokeResponseData: NoInvokeResponseData) {
+ logger.log(Level.INFO, "Batch Invoke receive onNoResponse on $noInvokeResponseData")
+ }
+
+ override fun onDone() {
+ if (responseCount == TEST_COMMONDS_NUM) {
+ setSuccess()
+ } else {
+ setFailure("invoke failure")
+ }
+ }
+ }
+
+ private inner class InternalGetConnectedDeviceCallback : GetConnectedDeviceCallback {
+ override fun onDeviceConnected(devicePointer: Long) {
+ setDevicePointer(devicePointer)
+ logger.log(Level.INFO, "onDeviceConnected")
+ }
+
+ override fun onConnectionFailure(nodeId: Long, error: Exception) {
+ logger.log(Level.INFO, "onConnectionFailure")
+ }
+ }
+
+ override fun runCommand() {
+ val number: UShort = 1u
+ val tlvWriter1 = TlvWriter()
+ tlvWriter1.startStructure(AnonymousTag)
+ tlvWriter1.put(ContextSpecificTag(0), number)
+ tlvWriter1.endStructure()
+
+ val element1: InvokeElement =
+ InvokeElement.newInstance(
+ /* endpointId= */ 0,
+ CLUSTER_ID_IDENTIFY,
+ IDENTIFY_COMMAND,
+ tlvWriter1.getEncoded(),
+ null
+ )
+
+ val tlvWriter2 = TlvWriter()
+ tlvWriter2.startStructure(AnonymousTag)
+ tlvWriter2.put(ContextSpecificTag(0), number)
+ tlvWriter2.put(ContextSpecificTag(1), number)
+ tlvWriter2.endStructure()
+
+ val element2: InvokeElement =
+ InvokeElement.newInstance(
+ /* endpointId= */ 1,
+ CLUSTER_ID_TEST,
+ TEST_ADD_ARGUMENT_COMMAND,
+ tlvWriter2.getEncoded(),
+ null
+ )
+
+ val invokeList = listOf(element1, element2)
+ currentCommissioner()
+ .pairDeviceWithAddress(
+ getNodeId(),
+ getRemoteAddr().address.hostAddress,
+ MATTER_PORT,
+ getDiscriminator(),
+ getSetupPINCode(),
+ null
+ )
+ currentCommissioner().setCompletionListener(this)
+ waitCompleteMs(getTimeoutMillis())
+ currentCommissioner()
+ .getConnectedDevicePointer(getNodeId(), InternalGetConnectedDeviceCallback())
+ clear()
+ currentCommissioner()
+ .extendableInvoke(InternalInvokeCallback(), devicePointer, invokeList, 0, 0)
+ waitCompleteMs(getTimeoutMillis())
+ }
+
+ companion object {
+ private val logger =
+ Logger.getLogger(PairOnNetworkLongImExtendableInvokeCommand::class.java.name)
+
+ private const val MATTER_PORT = 5540
+ private const val CLUSTER_ID_IDENTIFY = 0x0003L
+ private const val IDENTIFY_COMMAND = 0L
+ private const val CLUSTER_ID_TEST = 0xFFF1FC05L
+ private const val TEST_ADD_ARGUMENT_COMMAND = 0X04L
+ private const val TEST_ADD_ARGUMENT_RSP_COMMAND = 0X01L
+ private const val TEST_COMMONDS_NUM = 2
+ }
+}
diff --git a/kotlin-detect-config.yaml b/kotlin-detect-config.yaml
index 2ad54a0..c839829 100644
--- a/kotlin-detect-config.yaml
+++ b/kotlin-detect-config.yaml
@@ -104,6 +104,7 @@
- "**/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkInstanceNameCommand.kt"
- "**/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongCommand.kt"
- "**/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImInvokeCommand.kt"
+ - "**/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImExtendableInvokeCommand.kt"
- "**/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImWriteCommand.kt"
- "**/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkShortCommand.kt"
- "**/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkVendorCommand.kt"
diff --git a/scripts/tests/java/im_test.py b/scripts/tests/java/im_test.py
index d8acc63..6ac3936 100755
--- a/scripts/tests/java/im_test.py
+++ b/scripts/tests/java/im_test.py
@@ -71,6 +71,14 @@
DumpProgramOutputToQueue(self.thread_list, Fore.GREEN + "JAVA " + Style.RESET_ALL, java_process, self.queue)
return java_process.wait()
+ def TestCmdOnnetworkLongImExtendableInvoke(self, nodeid, setuppin, discriminator, timeout):
+ java_command = self.command + ['im', 'onnetwork-long-im-batch-invoke', nodeid, setuppin, discriminator, timeout]
+ logging.info(f"Execute: {java_command}")
+ java_process = subprocess.Popen(
+ java_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ DumpProgramOutputToQueue(self.thread_list, Fore.GREEN + "JAVA " + Style.RESET_ALL, java_process, self.queue)
+ return java_process.wait()
+
def TestCmdOnnetworkLongImWrite(self, nodeid, setuppin, discriminator, timeout):
java_command = self.command + ['im', 'onnetwork-long-im-write', nodeid, setuppin, discriminator, timeout]
logging.info(f"Execute: {java_command}")
@@ -101,6 +109,11 @@
code = self.TestCmdOnnetworkLongImInvoke(self.nodeid, self.setup_pin_code, self.discriminator, self.timeout)
if code != 0:
raise Exception(f"Testing pairing onnetwork-long-im-invoke failed with error {code}")
+ elif self.command_name == 'onnetwork-long-im-batch-invoke':
+ logging.info("Testing pairing onnetwork-long-im-batch-invoke")
+ code = self.TestCmdOnnetworkLongImExtendableInvoke(self.nodeid, self.setup_pin_code, self.discriminator, self.timeout)
+ if code != 0:
+ raise Exception(f"Testing pairing onnetwork-long-im-batch-invoke failed with error {code}")
elif self.command_name == 'onnetwork-long-im-write':
logging.info("Testing pairing onnetwork-long-im-write")
code = self.TestCmdOnnetworkLongImWrite(self.nodeid, self.setup_pin_code, self.discriminator, self.timeout)
diff --git a/src/controller/java/AndroidCallbacks-JNI.cpp b/src/controller/java/AndroidCallbacks-JNI.cpp
index c87b669..fff1226 100644
--- a/src/controller/java/AndroidCallbacks-JNI.cpp
+++ b/src/controller/java/AndroidCallbacks-JNI.cpp
@@ -69,3 +69,14 @@
{
deleteInvokeCallback(env, self, callbackHandle);
}
+
+JNI_METHOD(jlong, ExtendableInvokeCallbackJni, newCallback)
+(JNIEnv * env, jobject self)
+{
+ return newExtendableInvokeCallback(env, self);
+}
+
+JNI_METHOD(void, ExtendableInvokeCallbackJni, deleteCallback)(JNIEnv * env, jobject self, jlong callbackHandle)
+{
+ deleteExtendableInvokeCallback(env, self, callbackHandle);
+}
diff --git a/src/controller/java/AndroidCallbacks.cpp b/src/controller/java/AndroidCallbacks.cpp
index fb79752..fc9fbe0 100644
--- a/src/controller/java/AndroidCallbacks.cpp
+++ b/src/controller/java/AndroidCallbacks.cpp
@@ -791,6 +791,7 @@
if (mCommandSender != nullptr)
{
Platform::Delete(mCommandSender);
+ mCommandSender = nullptr;
}
}
@@ -802,7 +803,6 @@
VerifyOrReturn(env != nullptr, ChipLogError(Controller, "Could not get JNIEnv for current thread"));
jmethodID onResponseMethod;
JniLocalReferenceScope scope(env);
- VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Unable to create Java InvokeElement: %s", ErrorStr(err)));
VerifyOrReturn(mWrapperCallbackRef.HasValidObjectRef(),
ChipLogError(Controller, "mWrapperCallbackRef is not valid in %s", __func__));
jobject wrapperCallbackRef = mWrapperCallbackRef.ObjectRef();
@@ -1023,6 +1023,170 @@
}
}
+ExtendableInvokeCallback::ExtendableInvokeCallback(jobject wrapperCallback)
+{
+ VerifyOrReturn(mWrapperCallbackRef.Init(wrapperCallback) == CHIP_NO_ERROR,
+ ChipLogError(Controller, "Could not init mWrapperCallbackRef for ExtendableInvokeCallback"));
+}
+
+ExtendableInvokeCallback::~ExtendableInvokeCallback()
+{
+ if (mCommandSender != nullptr)
+ {
+ Platform::Delete(mCommandSender);
+ mCommandSender = nullptr;
+ }
+}
+
+void ExtendableInvokeCallback::OnResponse(app::CommandSender * apCommandSender,
+ const app::CommandSender::ResponseData & aResponseData)
+{
+ CHIP_ERROR err = CHIP_NO_ERROR;
+ JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread();
+ VerifyOrReturn(env != nullptr, ChipLogError(Controller, "Could not get JNIEnv for current thread"));
+ jmethodID onResponseMethod;
+ JniLocalReferenceScope scope(env);
+ VerifyOrReturn(mWrapperCallbackRef.HasValidObjectRef(),
+ ChipLogError(Controller, "mWrapperCallbackRef is not valid in %s", __func__));
+ jobject wrapperCallbackRef = mWrapperCallbackRef.ObjectRef();
+ DeviceLayer::StackUnlock unlock;
+
+ jobject jCommandRef = nullptr;
+ if (aResponseData.commandRef.HasValue())
+ {
+ err = JniReferences::GetInstance().CreateBoxedObject<jint>(
+ "java/lang/Integer", "(I)V", static_cast<jint>(aResponseData.commandRef.Value()), jCommandRef);
+ VerifyOrReturn(err == CHIP_NO_ERROR,
+ ChipLogError(Controller, "Could not CreateBoxedObject with error %" CHIP_ERROR_FORMAT, err.Format()));
+ }
+
+ if (aResponseData.data != nullptr)
+ {
+ TLV::TLVReader readerForJavaTLV;
+ TLV::TLVReader readerForJson;
+ readerForJavaTLV.Init(*(aResponseData.data));
+
+ // Create TLV byte array to pass to Java layer
+ size_t bufferLen = readerForJavaTLV.GetRemainingLength() + readerForJavaTLV.GetLengthRead();
+ std::unique_ptr<uint8_t[]> buffer = std::unique_ptr<uint8_t[]>(new uint8_t[bufferLen]);
+ uint32_t size = 0;
+
+ TLV::TLVWriter writer;
+ writer.Init(buffer.get(), bufferLen);
+ err = writer.CopyElement(TLV::AnonymousTag(), readerForJavaTLV);
+ VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Failed CopyElement: %" CHIP_ERROR_FORMAT, err.Format()));
+ size = writer.GetLengthWritten();
+
+ chip::ByteArray jniByteArray(env, reinterpret_cast<jbyte *>(buffer.get()), static_cast<jint>(size));
+
+ // Convert TLV to JSON
+ std::string json;
+ readerForJson.Init(buffer.get(), size);
+ err = readerForJson.Next();
+ VerifyOrReturn(err == CHIP_NO_ERROR,
+ ChipLogError(Controller, "Failed readerForJson next: %" CHIP_ERROR_FORMAT, err.Format()));
+ err = TlvToJson(readerForJson, json);
+ VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Failed TlvToJson: %" CHIP_ERROR_FORMAT, err.Format()));
+ UtfString jsonString(env, json.c_str());
+
+ err = JniReferences::GetInstance().FindMethod(env, wrapperCallbackRef, "onResponse",
+ "(IJJLjava/lang/Integer;[BLjava/lang/String;)V", &onResponseMethod);
+ VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Unable to find onResponse method: %s", ErrorStr(err)));
+
+ env->CallVoidMethod(wrapperCallbackRef, onResponseMethod, static_cast<jint>(aResponseData.path.mEndpointId),
+ static_cast<jlong>(aResponseData.path.mClusterId), static_cast<jlong>(aResponseData.path.mCommandId),
+ jCommandRef, jniByteArray.jniValue(), jsonString.jniValue());
+ }
+ else
+ {
+ err = JniReferences::GetInstance().FindMethod(env, wrapperCallbackRef, "onResponse",
+ "(IJJLjava/lang/Integer;ILjava/lang/Integer;)V", &onResponseMethod);
+ VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Unable to find onResponse method: %s", ErrorStr(err)));
+
+ jobject jClusterState = nullptr;
+ if (aResponseData.statusIB.mClusterStatus.HasValue())
+ {
+ err = JniReferences::GetInstance().CreateBoxedObject<jint>(
+ "java/lang/Integer", "(I)V", static_cast<jint>(aResponseData.statusIB.mClusterStatus.Value()), jClusterState);
+ VerifyOrReturn(err == CHIP_NO_ERROR,
+ ChipLogError(Controller, "Could not CreateBoxedObject with error %" CHIP_ERROR_FORMAT, err.Format()));
+ }
+
+ env->CallVoidMethod(wrapperCallbackRef, onResponseMethod, static_cast<jint>(aResponseData.path.mEndpointId),
+ static_cast<jlong>(aResponseData.path.mClusterId), static_cast<jlong>(aResponseData.path.mCommandId),
+ jCommandRef, aResponseData.statusIB.mStatus, jClusterState);
+ }
+
+ VerifyOrReturn(!env->ExceptionCheck(), env->ExceptionDescribe());
+}
+
+void ExtendableInvokeCallback::OnNoResponse(app::CommandSender * commandSender,
+ const app::CommandSender::NoResponseData & aNoResponseData)
+{
+ CHIP_ERROR err = CHIP_NO_ERROR;
+ JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread();
+ VerifyOrReturn(env != nullptr, ChipLogError(Controller, "Could not get JNIEnv for current thread"));
+ jmethodID onNoResponseMethod;
+ JniLocalReferenceScope scope(env);
+ VerifyOrReturn(mWrapperCallbackRef.HasValidObjectRef(),
+ ChipLogError(Controller, "mWrapperCallbackRef is not valid in %s", __func__));
+ jobject wrapperCallbackRef = mWrapperCallbackRef.ObjectRef();
+ DeviceLayer::StackUnlock unlock;
+
+ err = JniReferences::GetInstance().FindMethod(env, wrapperCallbackRef, "onNoResponse", "(I)V", &onNoResponseMethod);
+ VerifyOrReturn(err == CHIP_NO_ERROR,
+ ChipLogError(Controller, "Unable to find onNoResponse method: %" CHIP_ERROR_FORMAT, err.Format()));
+ env->CallVoidMethod(wrapperCallbackRef, onNoResponseMethod, static_cast<jint>(aNoResponseData.commandRef));
+ VerifyOrReturn(!env->ExceptionCheck(), env->ExceptionDescribe());
+}
+
+void ExtendableInvokeCallback::OnError(const app::CommandSender * apCommandSender, const app::CommandSender::ErrorData & aErrorData)
+{
+ CHIP_ERROR err = CHIP_NO_ERROR;
+ JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread();
+ VerifyOrReturn(env != nullptr, ChipLogError(Controller, "Could not get JNIEnv for current thread"));
+ JniLocalReferenceScope scope(env);
+ ChipLogError(Controller, "ExtendableInvokeCallback::OnError is called with %u", aErrorData.error.AsInteger());
+ jthrowable exception;
+ err = AndroidControllerExceptions::GetInstance().CreateAndroidControllerException(env, ErrorStr(aErrorData.error),
+ aErrorData.error.AsInteger(), exception);
+ VerifyOrReturn(
+ err == CHIP_NO_ERROR,
+ ChipLogError(Controller, "Unable to create AndroidControllerException with error: %" CHIP_ERROR_FORMAT, err.Format()));
+
+ jmethodID onErrorMethod;
+ VerifyOrReturn(mWrapperCallbackRef.HasValidObjectRef(),
+ ChipLogError(Controller, "mWrapperCallbackRef is not valid in %s", __func__));
+ jobject wrapperCallback = mWrapperCallbackRef.ObjectRef();
+ err = JniReferences::GetInstance().FindMethod(env, wrapperCallback, "onError", "(Ljava/lang/Exception;)V", &onErrorMethod);
+ VerifyOrReturn(err == CHIP_NO_ERROR,
+ ChipLogError(Controller, "Unable to find onError method: %" CHIP_ERROR_FORMAT, err.Format()));
+
+ DeviceLayer::StackUnlock unlock;
+ env->CallVoidMethod(wrapperCallback, onErrorMethod, exception);
+ VerifyOrReturn(!env->ExceptionCheck(), env->ExceptionDescribe());
+}
+
+void ExtendableInvokeCallback::OnDone(app::CommandSender * apCommandSender)
+{
+ CHIP_ERROR err = CHIP_NO_ERROR;
+ JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread();
+ VerifyOrReturn(env != nullptr, ChipLogError(Controller, "Could not get JNIEnv for current thread"));
+ JniLocalReferenceScope scope(env);
+ jmethodID onDoneMethod;
+ VerifyOrReturn(mWrapperCallbackRef.HasValidObjectRef(),
+ ChipLogError(Controller, "mWrapperCallbackRef is not valid in %s", __func__));
+ jobject wrapperCallback = mWrapperCallbackRef.ObjectRef();
+ JniGlobalReference globalRef(std::move(mWrapperCallbackRef));
+
+ err = JniReferences::GetInstance().FindMethod(env, wrapperCallback, "onDone", "()V", &onDoneMethod);
+ VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Could not find onDone method"));
+
+ DeviceLayer::StackUnlock unlock;
+ env->CallVoidMethod(wrapperCallback, onDoneMethod);
+ VerifyOrReturn(!env->ExceptionCheck(), env->ExceptionDescribe());
+}
+
jlong newConnectedDeviceCallback(JNIEnv * env, jobject self, jobject callback)
{
chip::DeviceLayer::StackLock lock;
@@ -1085,5 +1249,19 @@
chip::Platform::Delete(invokeCallback);
}
+jlong newExtendableInvokeCallback(JNIEnv * env, jobject self)
+{
+ chip::DeviceLayer::StackLock lock;
+ ExtendableInvokeCallback * invokeCallback = chip::Platform::New<ExtendableInvokeCallback>(self);
+ return reinterpret_cast<jlong>(invokeCallback);
+}
+
+void deleteExtendableInvokeCallback(JNIEnv * env, jobject self, jlong callbackHandle)
+{
+ chip::DeviceLayer::StackLock lock;
+ ExtendableInvokeCallback * invokeCallback = reinterpret_cast<ExtendableInvokeCallback *>(callbackHandle);
+ VerifyOrReturn(invokeCallback != nullptr, ChipLogError(Controller, "ExtendableInvokeCallback handle is nullptr"));
+ chip::Platform::Delete(invokeCallback);
+}
} // namespace Controller
} // namespace chip
diff --git a/src/controller/java/AndroidCallbacks.h b/src/controller/java/AndroidCallbacks.h
index 700a1fb..0c49daf 100644
--- a/src/controller/java/AndroidCallbacks.h
+++ b/src/controller/java/AndroidCallbacks.h
@@ -25,6 +25,7 @@
#include <lib/core/CHIPError.h>
#include <lib/support/JniTypeWrappers.h>
#include <list>
+#include <unordered_map>
#include <utility>
namespace chip {
@@ -138,6 +139,20 @@
JniGlobalReference mWrapperCallbackRef;
};
+struct ExtendableInvokeCallback : public app::CommandSender::ExtendableCallback
+{
+ ExtendableInvokeCallback(jobject wrapperCallback);
+ ~ExtendableInvokeCallback();
+
+ void OnResponse(app::CommandSender * commandSender, const app::CommandSender::ResponseData & aResponseData) override;
+ void OnNoResponse(app::CommandSender * commandSender, const app::CommandSender::NoResponseData & aNoResponseData) override;
+ void OnError(const app::CommandSender * apCommandSender, const app::CommandSender::ErrorData & aErrorData) override;
+ void OnDone(app::CommandSender * apCommandSender) override;
+
+ app::CommandSender * mCommandSender = nullptr;
+ JniGlobalReference mWrapperCallbackRef;
+};
+
jlong newConnectedDeviceCallback(JNIEnv * env, jobject self, jobject callback);
void deleteConnectedDeviceCallback(JNIEnv * env, jobject self, jlong callbackHandle);
jlong newReportCallback(JNIEnv * env, jobject self, jobject subscriptionEstablishedCallbackJava,
@@ -147,6 +162,8 @@
void deleteWriteAttributesCallback(JNIEnv * env, jobject self, jlong callbackHandle);
jlong newInvokeCallback(JNIEnv * env, jobject self);
void deleteInvokeCallback(JNIEnv * env, jobject self, jlong callbackHandle);
+jlong newExtendableInvokeCallback(JNIEnv * env, jobject self);
+void deleteExtendableInvokeCallback(JNIEnv * env, jobject self, jlong callbackHandle);
} // namespace Controller
} // namespace chip
diff --git a/src/controller/java/AndroidInteractionClient.cpp b/src/controller/java/AndroidInteractionClient.cpp
index a0b22bf..ec80957 100644
--- a/src/controller/java/AndroidInteractionClient.cpp
+++ b/src/controller/java/AndroidInteractionClient.cpp
@@ -431,7 +431,7 @@
CHIP_ERROR PutPreencodedInvokeRequest(app::CommandSender & commandSender, app::CommandPathParams & path, const ByteSpan & data)
{
- // PrepareCommand does nott create the struct container with kFields and copycontainer below sets the
+ // PrepareCommand does not create the struct container with kFields and copycontainer below sets the
// kFields container already
ReturnErrorOnFailure(commandSender.PrepareCommand(path, false /* aStartDataStruct */));
TLV::TLVWriter * writer = commandSender.GetCommandDataIBTLVWriter();
@@ -442,6 +442,191 @@
return writer->CopyContainer(TLV::ContextTag(app::CommandDataIB::Tag::kFields), reader);
}
+CHIP_ERROR PutPreencodedInvokeRequest(app::CommandSender & commandSender, app::CommandPathParams & path, const ByteSpan & data,
+ app::CommandSender::PrepareCommandParameters & prepareCommandParams)
+{
+ // PrepareCommand does not create the struct container with kFields and copycontainer below sets the
+ // kFields container already
+ ReturnErrorOnFailure(commandSender.PrepareCommand(path, prepareCommandParams));
+ TLV::TLVWriter * writer = commandSender.GetCommandDataIBTLVWriter();
+ VerifyOrReturnError(writer != nullptr, CHIP_ERROR_INCORRECT_STATE);
+ TLV::TLVReader reader;
+ reader.Init(data);
+ ReturnErrorOnFailure(reader.Next());
+ return writer->CopyContainer(TLV::ContextTag(app::CommandDataIB::Tag::kFields), reader);
+}
+
+CHIP_ERROR extendableInvoke(JNIEnv * env, jlong handle, jlong callbackHandle, jlong devicePtr, jobject invokeElementList,
+ jint timedRequestTimeoutMs, jint imTimeoutMs)
+{
+ chip::DeviceLayer::StackLock lock;
+ CHIP_ERROR err = CHIP_NO_ERROR;
+ auto callback = reinterpret_cast<ExtendableInvokeCallback *>(callbackHandle);
+ app::CommandSender * commandSender = nullptr;
+ uint16_t groupId = 0;
+ bool isEndpointIdValid = false;
+ bool isGroupIdValid = false;
+ jint listSize = 0;
+ uint16_t convertedTimedRequestTimeoutMs = static_cast<uint16_t>(timedRequestTimeoutMs);
+ app::CommandSender::ConfigParameters config;
+
+ ChipLogDetail(Controller, "IM extendableInvoke() called");
+
+ DeviceProxy * device = reinterpret_cast<DeviceProxy *>(devicePtr);
+ VerifyOrExit(device != nullptr, err = CHIP_ERROR_INCORRECT_STATE);
+ VerifyOrExit(device->GetSecureSession().HasValue(), err = CHIP_ERROR_MISSING_SECURE_SESSION);
+
+ VerifyOrExit(invokeElementList != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT);
+ SuccessOrExit(err = JniReferences::GetInstance().GetListSize(invokeElementList, listSize));
+
+ if ((listSize > 1) && (device->GetSecureSession().Value()->IsGroupSession()))
+ {
+ ChipLogError(Controller, "Not allow group session for InvokeRequests that has more than 1 CommandDataIB)");
+ err = CHIP_ERROR_INVALID_ARGUMENT;
+ goto exit;
+ }
+
+ commandSender = Platform::New<app::CommandSender>(callback, device->GetExchangeManager(), timedRequestTimeoutMs != 0);
+ config.SetRemoteMaxPathsPerInvoke(device->GetSecureSession().Value()->GetRemoteSessionParameters().GetMaxPathsPerInvoke());
+ SuccessOrExit(err = commandSender->SetCommandSenderConfig(config));
+
+ for (uint8_t i = 0; i < listSize; i++)
+ {
+ jmethodID getEndpointIdMethod = nullptr;
+ jmethodID getClusterIdMethod = nullptr;
+ jmethodID getCommandIdMethod = nullptr;
+ jmethodID getGroupIdMethod = nullptr;
+ jmethodID getTlvByteArrayMethod = nullptr;
+ jmethodID getJsonStringMethod = nullptr;
+ jmethodID isEndpointIdValidMethod = nullptr;
+ jmethodID isGroupIdValidMethod = nullptr;
+ jlong endpointIdObj = 0;
+ jlong clusterIdObj = 0;
+ jlong commandIdObj = 0;
+ jobject groupIdObj = nullptr;
+ jbyteArray tlvBytesObj = nullptr;
+ jobject invokeElement = nullptr;
+ SuccessOrExit(err = JniReferences::GetInstance().GetListItem(invokeElementList, i, invokeElement));
+ SuccessOrExit(
+ err = JniReferences::GetInstance().FindMethod(env, invokeElement, "getEndpointId", "(J)J", &getEndpointIdMethod));
+ SuccessOrExit(err =
+ JniReferences::GetInstance().FindMethod(env, invokeElement, "getClusterId", "(J)J", &getClusterIdMethod));
+ SuccessOrExit(err =
+ JniReferences::GetInstance().FindMethod(env, invokeElement, "getCommandId", "(J)J", &getCommandIdMethod));
+ SuccessOrExit(err = JniReferences::GetInstance().FindMethod(env, invokeElement, "getGroupId", "()Ljava/util/Optional;",
+ &getGroupIdMethod));
+ SuccessOrExit(err = JniReferences::GetInstance().FindMethod(env, invokeElement, "isEndpointIdValid", "()Z",
+ &isEndpointIdValidMethod));
+ SuccessOrExit(
+ err = JniReferences::GetInstance().FindMethod(env, invokeElement, "isGroupIdValid", "()Z", &isGroupIdValidMethod));
+ SuccessOrExit(
+ err = JniReferences::GetInstance().FindMethod(env, invokeElement, "getTlvByteArray", "()[B", &getTlvByteArrayMethod));
+
+ isEndpointIdValid = (env->CallBooleanMethod(invokeElement, isEndpointIdValidMethod) == JNI_TRUE);
+ isGroupIdValid = (env->CallBooleanMethod(invokeElement, isGroupIdValidMethod) == JNI_TRUE);
+
+ if (isEndpointIdValid)
+ {
+ endpointIdObj = env->CallLongMethod(invokeElement, getEndpointIdMethod, static_cast<jlong>(kInvalidEndpointId));
+ VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN);
+ }
+
+ if (isGroupIdValid)
+ {
+ VerifyOrExit(device->GetSecureSession().Value()->IsGroupSession(), err = CHIP_ERROR_INVALID_ARGUMENT);
+ groupIdObj = env->CallObjectMethod(invokeElement, getGroupIdMethod);
+ VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN);
+ VerifyOrExit(groupIdObj != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT);
+
+ jobject boxedGroupId = nullptr;
+
+ SuccessOrExit(err = JniReferences::GetInstance().GetOptionalValue(groupIdObj, boxedGroupId));
+ VerifyOrExit(boxedGroupId != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT);
+ groupId = static_cast<uint16_t>(JniReferences::GetInstance().IntegerToPrimitive(boxedGroupId));
+ }
+
+ clusterIdObj = env->CallLongMethod(invokeElement, getClusterIdMethod, static_cast<jlong>(kInvalidClusterId));
+ VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN);
+
+ commandIdObj = env->CallLongMethod(invokeElement, getCommandIdMethod, static_cast<jlong>(kInvalidCommandId));
+ VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN);
+
+ tlvBytesObj = static_cast<jbyteArray>(env->CallObjectMethod(invokeElement, getTlvByteArrayMethod));
+ VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN);
+
+ app::CommandSender::PrepareCommandParameters prepareCommandParams;
+ prepareCommandParams.commandRef.SetValue(static_cast<uint16_t>(i));
+
+ {
+ uint16_t id = isEndpointIdValid ? static_cast<uint16_t>(endpointIdObj) : groupId;
+ app::CommandPathFlags flag =
+ isEndpointIdValid ? app::CommandPathFlags::kEndpointIdValid : app::CommandPathFlags::kGroupIdValid;
+ app::CommandPathParams path(id, static_cast<ClusterId>(clusterIdObj), static_cast<CommandId>(commandIdObj), flag);
+
+ if (tlvBytesObj != nullptr)
+ {
+ JniByteArray tlvBytesObjBytes(env, tlvBytesObj);
+ SuccessOrExit(
+ err = PutPreencodedInvokeRequest(*commandSender, path, tlvBytesObjBytes.byteSpan(), prepareCommandParams));
+ }
+ else
+ {
+ SuccessOrExit(err = JniReferences::GetInstance().FindMethod(env, invokeElement, "getJsonString",
+ "()Ljava/lang/String;", &getJsonStringMethod));
+ jstring jsonJniString = static_cast<jstring>(env->CallObjectMethod(invokeElement, getJsonStringMethod));
+ VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN);
+ VerifyOrExit(jsonJniString != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT);
+ JniUtfString jsonUtfJniString(env, jsonJniString);
+ // The invoke does not support chunk, kMaxSecureSduLengthBytes should be enough for command json blob
+ uint8_t tlvBytes[chip::app::kMaxSecureSduLengthBytes] = { 0 };
+ MutableByteSpan tlvEncodingLocal{ tlvBytes };
+ SuccessOrExit(err = JsonToTlv(std::string(jsonUtfJniString.c_str(), static_cast<size_t>(jsonUtfJniString.size())),
+ tlvEncodingLocal));
+ SuccessOrExit(err = PutPreencodedInvokeRequest(*commandSender, path, tlvEncodingLocal, prepareCommandParams));
+ }
+ }
+
+ app::CommandSender::FinishCommandParameters finishCommandParams(convertedTimedRequestTimeoutMs != 0
+ ? Optional<uint16_t>(convertedTimedRequestTimeoutMs)
+ : Optional<uint16_t>::Missing());
+
+ finishCommandParams.commandRef = prepareCommandParams.commandRef;
+ SuccessOrExit(err = commandSender->FinishCommand(finishCommandParams));
+ }
+ SuccessOrExit(err = device->GetSecureSession().Value()->IsGroupSession()
+ ? commandSender->SendGroupCommandRequest(device->GetSecureSession().Value())
+ : commandSender->SendCommandRequest(device->GetSecureSession().Value(),
+ imTimeoutMs != 0
+ ? MakeOptional(System::Clock::Milliseconds32(imTimeoutMs))
+ : Optional<System::Clock::Timeout>::Missing()));
+
+ callback->mCommandSender = commandSender;
+exit:
+ if (err != CHIP_NO_ERROR)
+ {
+ ChipLogError(Controller, "JNI IM Invoke Error: %s", err.AsString());
+ if (err == CHIP_JNI_ERROR_EXCEPTION_THROWN)
+ {
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ }
+ app::CommandSender::ErrorData errorData;
+ errorData.error = err;
+ callback->OnError(nullptr, errorData);
+ if (commandSender != nullptr)
+ {
+ Platform::Delete(commandSender);
+ commandSender = nullptr;
+ }
+ if (callback != nullptr)
+ {
+ Platform::Delete(callback);
+ callback = nullptr;
+ }
+ }
+ return err;
+}
+
CHIP_ERROR invoke(JNIEnv * env, jlong handle, jlong callbackHandle, jlong devicePtr, jobject invokeElement,
jint timedRequestTimeoutMs, jint imTimeoutMs)
{
diff --git a/src/controller/java/AndroidInteractionClient.h b/src/controller/java/AndroidInteractionClient.h
index 095061f..f38af6f 100644
--- a/src/controller/java/AndroidInteractionClient.h
+++ b/src/controller/java/AndroidInteractionClient.h
@@ -29,3 +29,5 @@
jint timedRequestTimeoutMs, jint imTimeoutMs);
CHIP_ERROR invoke(JNIEnv * env, jlong handle, jlong callbackHandle, jlong devicePtr, jobject invokeElement,
jint timedRequestTimeoutMs, jint imTimeoutMs);
+CHIP_ERROR extendableInvoke(JNIEnv * env, jlong handle, jlong callbackHandle, jlong devicePtr, jobject invokeElementList,
+ jint timedRequestTimeoutMs, jint imTimeoutMs);
diff --git a/src/controller/java/BUILD.gn b/src/controller/java/BUILD.gn
index 31dbaaf..a2f3c86 100644
--- a/src/controller/java/BUILD.gn
+++ b/src/controller/java/BUILD.gn
@@ -459,6 +459,8 @@
"src/chip/devicecontroller/ControllerParams.java",
"src/chip/devicecontroller/DeviceAttestationDelegate.java",
"src/chip/devicecontroller/DiscoveredDevice.java",
+ "src/chip/devicecontroller/ExtendableInvokeCallback.java",
+ "src/chip/devicecontroller/ExtendableInvokeCallbackJni.java",
"src/chip/devicecontroller/GetConnectedDeviceCallbackJni.java",
"src/chip/devicecontroller/GroupKeySecurityPolicy.java",
"src/chip/devicecontroller/ICDClientInfo.java",
@@ -493,6 +495,8 @@
"src/chip/devicecontroller/model/EndpointState.java",
"src/chip/devicecontroller/model/EventState.java",
"src/chip/devicecontroller/model/InvokeElement.java",
+ "src/chip/devicecontroller/model/InvokeResponseData.java",
+ "src/chip/devicecontroller/model/NoInvokeResponseData.java",
"src/chip/devicecontroller/model/NodeState.java",
"src/chip/devicecontroller/model/Status.java",
]
diff --git a/src/controller/java/CHIPDeviceController-JNI.cpp b/src/controller/java/CHIPDeviceController-JNI.cpp
index 206103c..fc2c069 100644
--- a/src/controller/java/CHIPDeviceController-JNI.cpp
+++ b/src/controller/java/CHIPDeviceController-JNI.cpp
@@ -2285,6 +2285,18 @@
}
}
+JNI_METHOD(void, extendableInvoke)
+(JNIEnv * env, jclass clz, jlong handle, jlong callbackHandle, jlong devicePtr, jobject invokeElementList,
+ jint timedRequestTimeoutMs, jint imTimeoutMs)
+{
+ CHIP_ERROR err =
+ extendableInvoke(env, handle, callbackHandle, devicePtr, invokeElementList, timedRequestTimeoutMs, imTimeoutMs);
+ if (err != CHIP_NO_ERROR)
+ {
+ ChipLogError(Controller, "JNI IM Batch Invoke Error: %" CHIP_ERROR_FORMAT, err.Format());
+ }
+}
+
void * IOThreadMain(void * arg)
{
JNIEnv * env;
diff --git a/src/controller/java/MatterCallbacks-JNI.cpp b/src/controller/java/MatterCallbacks-JNI.cpp
index f20d9e4..93101df 100644
--- a/src/controller/java/MatterCallbacks-JNI.cpp
+++ b/src/controller/java/MatterCallbacks-JNI.cpp
@@ -69,3 +69,14 @@
{
deleteInvokeCallback(env, self, callbackHandle);
}
+
+JNI_METHOD(jlong, ExtendableInvokeCallbackJni, newCallback)
+(JNIEnv * env, jobject self)
+{
+ return newExtendableInvokeCallback(env, self);
+}
+
+JNI_METHOD(void, ExtendableInvokeCallbackJni, deleteCallback)(JNIEnv * env, jobject self, jlong callbackHandle)
+{
+ deleteExtendableInvokeCallback(env, self, callbackHandle);
+}
diff --git a/src/controller/java/MatterInteractionClient-JNI.cpp b/src/controller/java/MatterInteractionClient-JNI.cpp
index 6a54172..152e52a 100644
--- a/src/controller/java/MatterInteractionClient-JNI.cpp
+++ b/src/controller/java/MatterInteractionClient-JNI.cpp
@@ -65,3 +65,15 @@
ChipLogError(Controller, "JNI IM Invoke Error: %" CHIP_ERROR_FORMAT, err.Format());
}
}
+
+JNI_METHOD(void, extendableInvoke)
+(JNIEnv * env, jobject self, jlong handle, jlong callbackHandle, jlong devicePtr, jobject invokeElementList,
+ jint timedRequestTimeoutMs, jint imTimeoutMs)
+{
+ CHIP_ERROR err =
+ extendableInvoke(env, handle, callbackHandle, devicePtr, invokeElementList, timedRequestTimeoutMs, imTimeoutMs);
+ if (err != CHIP_NO_ERROR)
+ {
+ ChipLogError(Controller, "JNI IM Batch Invoke Error: %" CHIP_ERROR_FORMAT, err.Format());
+ }
+}
diff --git a/src/controller/java/src/chip/devicecontroller/BatchInvokeCallback.java b/src/controller/java/src/chip/devicecontroller/BatchInvokeCallback.java
new file mode 100644
index 0000000..1b07e38
--- /dev/null
+++ b/src/controller/java/src/chip/devicecontroller/BatchInvokeCallback.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2024 Project CHIP Authors
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package chip.devicecontroller;
+
+import chip.devicecontroller.model.InvokeResponseData;
+import chip.devicecontroller.model.NoInvokeResponseData;
+
+/** An interface for receiving invoke response. */
+public interface ExtendableInvokeCallback {
+
+ /**
+ * OnError will be called when an error occurs after failing to call
+ *
+ * @param Exception The IllegalStateException which encapsulated the error message, the possible
+ * chip error could be - CHIP_ERROR_TIMEOUT: A response was not received within the expected
+ * response timeout. - CHIP_ERROR_*TLV*: A malformed, non-compliant response was received from
+ * the server. - CHIP_ERROR encapsulating the converted error from the StatusIB: If we got a
+ * non-path-specific status response from the server. - CHIP_ERROR*: All other cases.
+ */
+ void onError(Exception e);
+
+ /**
+ * OnResponse will be called when a write response has been received and processed for the given
+ * path.
+ *
+ * @param invokeResponseData invoke response that has either payload or status
+ */
+ void onResponse(InvokeResponseData invokeResponseData);
+
+ /**
+ * onNoResponse will be called for each request that failed to receive a response after the server
+ * indicates completion of all requests.
+ *
+ * @param noInvokeResponseData failed response data
+ */
+ void onNoResponse(NoInvokeResponseData noInvokeResponseData);
+
+ void onDone();
+}
diff --git a/src/controller/java/src/chip/devicecontroller/BatchInvokeCallbackJni.java b/src/controller/java/src/chip/devicecontroller/BatchInvokeCallbackJni.java
new file mode 100644
index 0000000..6982f58
--- /dev/null
+++ b/src/controller/java/src/chip/devicecontroller/BatchInvokeCallbackJni.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2024 Project CHIP Authors
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package chip.devicecontroller;
+
+import chip.devicecontroller.model.InvokeResponseData;
+import chip.devicecontroller.model.NoInvokeResponseData;
+import java.util.Optional;
+import javax.annotation.Nullable;
+
+/** JNI wrapper callback class for {@link InvokeCallback}. */
+public final class ExtendableInvokeCallbackJni {
+ private final ExtendableInvokeCallback wrappedExtendableInvokeCallback;
+ private long callbackHandle;
+
+ public ExtendableInvokeCallbackJni(ExtendableInvokeCallback wrappedExtendableInvokeCallback) {
+ this.wrappedExtendableInvokeCallback = wrappedExtendableInvokeCallback;
+ this.callbackHandle = newCallback();
+ }
+
+ long getCallbackHandle() {
+ return callbackHandle;
+ }
+
+ private native long newCallback();
+
+ private native void deleteCallback(long callbackHandle);
+
+ private void onError(Exception e) {
+ wrappedExtendableInvokeCallback.onError(e);
+ }
+
+ private void onResponse(
+ int endpointId,
+ long clusterId,
+ long commandId,
+ @Nullable Integer commandRef,
+ byte[] tlv,
+ String jsonString) {
+ wrappedExtendableInvokeCallback.onResponse(
+ InvokeResponseData.newInstance(
+ endpointId, clusterId, commandId, Optional.ofNullable(commandRef), tlv, jsonString));
+ }
+
+ private void onResponse(
+ int endpointId,
+ long clusterId,
+ long commandId,
+ @Nullable Integer commandRef,
+ int status,
+ @Nullable Integer clusterStatus) {
+ wrappedExtendableInvokeCallback.onResponse(
+ InvokeResponseData.newInstance(
+ endpointId,
+ clusterId,
+ commandId,
+ Optional.ofNullable(commandRef),
+ status,
+ Optional.ofNullable(clusterStatus)));
+ }
+
+ private void onNoResponse(int commandRef) {
+ wrappedExtendableInvokeCallback.onNoResponse(NoInvokeResponseData.newInstance(commandRef));
+ }
+
+ private void onDone() {
+ wrappedExtendableInvokeCallback.onDone();
+ }
+
+ // TODO(#8578): Replace finalizer with PhantomReference.
+ @SuppressWarnings("deprecation")
+ protected void finalize() throws Throwable {
+ super.finalize();
+
+ if (callbackHandle != 0) {
+ deleteCallback(callbackHandle);
+ callbackHandle = 0;
+ }
+ }
+}
diff --git a/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java b/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java
index 9e5c6e1..25d300e 100644
--- a/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java
+++ b/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java
@@ -1205,6 +1205,32 @@
imTimeoutMs);
}
+ /**
+ * @brief ExtendableInvoke command to target device
+ * @param ExtendableInvokeCallback Callback when invoke responses have been received and processed
+ * for the given batched invoke commands.
+ * @param devicePtr connected device pointer
+ * @param invokeElementList invoke element list
+ * @param timedRequestTimeoutMs this is timed request if this value is larger than 0
+ * @param imTimeoutMs im interaction time out value, it would override the default value in c++ im
+ * layer if this value is non-zero.
+ */
+ public void extendableInvoke(
+ ExtendableInvokeCallback callback,
+ long devicePtr,
+ List<InvokeElement> invokeElementList,
+ int timedRequestTimeoutMs,
+ int imTimeoutMs) {
+ ExtendableInvokeCallbackJni jniCallback = new ExtendableInvokeCallbackJni(callback);
+ extendableInvoke(
+ deviceControllerPtr,
+ jniCallback.getCallbackHandle(),
+ devicePtr,
+ invokeElementList,
+ timedRequestTimeoutMs,
+ imTimeoutMs);
+ }
+
/** Create a root (self-signed) X.509 DER encoded certificate */
public static byte[] createRootCertificate(
KeypairDelegate keypair, long issuerId, @Nullable Long fabricId) {
@@ -1377,6 +1403,14 @@
int timedRequestTimeoutMs,
int imTimeoutMs);
+ static native void extendableInvoke(
+ long deviceControllerPtr,
+ long callbackHandle,
+ long devicePtr,
+ List<InvokeElement> invokeElementList,
+ int timedRequestTimeoutMs,
+ int imTimeoutMs);
+
private native long newDeviceController(ControllerParams params);
private native void setDeviceAttestationDelegate(
diff --git a/src/controller/java/src/chip/devicecontroller/ExtendableInvokeCallback.java b/src/controller/java/src/chip/devicecontroller/ExtendableInvokeCallback.java
new file mode 100644
index 0000000..1b07e38
--- /dev/null
+++ b/src/controller/java/src/chip/devicecontroller/ExtendableInvokeCallback.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2024 Project CHIP Authors
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package chip.devicecontroller;
+
+import chip.devicecontroller.model.InvokeResponseData;
+import chip.devicecontroller.model.NoInvokeResponseData;
+
+/** An interface for receiving invoke response. */
+public interface ExtendableInvokeCallback {
+
+ /**
+ * OnError will be called when an error occurs after failing to call
+ *
+ * @param Exception The IllegalStateException which encapsulated the error message, the possible
+ * chip error could be - CHIP_ERROR_TIMEOUT: A response was not received within the expected
+ * response timeout. - CHIP_ERROR_*TLV*: A malformed, non-compliant response was received from
+ * the server. - CHIP_ERROR encapsulating the converted error from the StatusIB: If we got a
+ * non-path-specific status response from the server. - CHIP_ERROR*: All other cases.
+ */
+ void onError(Exception e);
+
+ /**
+ * OnResponse will be called when a write response has been received and processed for the given
+ * path.
+ *
+ * @param invokeResponseData invoke response that has either payload or status
+ */
+ void onResponse(InvokeResponseData invokeResponseData);
+
+ /**
+ * onNoResponse will be called for each request that failed to receive a response after the server
+ * indicates completion of all requests.
+ *
+ * @param noInvokeResponseData failed response data
+ */
+ void onNoResponse(NoInvokeResponseData noInvokeResponseData);
+
+ void onDone();
+}
diff --git a/src/controller/java/src/chip/devicecontroller/ExtendableInvokeCallbackJni.java b/src/controller/java/src/chip/devicecontroller/ExtendableInvokeCallbackJni.java
new file mode 100644
index 0000000..6982f58
--- /dev/null
+++ b/src/controller/java/src/chip/devicecontroller/ExtendableInvokeCallbackJni.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2024 Project CHIP Authors
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package chip.devicecontroller;
+
+import chip.devicecontroller.model.InvokeResponseData;
+import chip.devicecontroller.model.NoInvokeResponseData;
+import java.util.Optional;
+import javax.annotation.Nullable;
+
+/** JNI wrapper callback class for {@link InvokeCallback}. */
+public final class ExtendableInvokeCallbackJni {
+ private final ExtendableInvokeCallback wrappedExtendableInvokeCallback;
+ private long callbackHandle;
+
+ public ExtendableInvokeCallbackJni(ExtendableInvokeCallback wrappedExtendableInvokeCallback) {
+ this.wrappedExtendableInvokeCallback = wrappedExtendableInvokeCallback;
+ this.callbackHandle = newCallback();
+ }
+
+ long getCallbackHandle() {
+ return callbackHandle;
+ }
+
+ private native long newCallback();
+
+ private native void deleteCallback(long callbackHandle);
+
+ private void onError(Exception e) {
+ wrappedExtendableInvokeCallback.onError(e);
+ }
+
+ private void onResponse(
+ int endpointId,
+ long clusterId,
+ long commandId,
+ @Nullable Integer commandRef,
+ byte[] tlv,
+ String jsonString) {
+ wrappedExtendableInvokeCallback.onResponse(
+ InvokeResponseData.newInstance(
+ endpointId, clusterId, commandId, Optional.ofNullable(commandRef), tlv, jsonString));
+ }
+
+ private void onResponse(
+ int endpointId,
+ long clusterId,
+ long commandId,
+ @Nullable Integer commandRef,
+ int status,
+ @Nullable Integer clusterStatus) {
+ wrappedExtendableInvokeCallback.onResponse(
+ InvokeResponseData.newInstance(
+ endpointId,
+ clusterId,
+ commandId,
+ Optional.ofNullable(commandRef),
+ status,
+ Optional.ofNullable(clusterStatus)));
+ }
+
+ private void onNoResponse(int commandRef) {
+ wrappedExtendableInvokeCallback.onNoResponse(NoInvokeResponseData.newInstance(commandRef));
+ }
+
+ private void onDone() {
+ wrappedExtendableInvokeCallback.onDone();
+ }
+
+ // TODO(#8578): Replace finalizer with PhantomReference.
+ @SuppressWarnings("deprecation")
+ protected void finalize() throws Throwable {
+ super.finalize();
+
+ if (callbackHandle != 0) {
+ deleteCallback(callbackHandle);
+ callbackHandle = 0;
+ }
+ }
+}
diff --git a/src/controller/java/src/chip/devicecontroller/model/InvokeResponseData.java b/src/controller/java/src/chip/devicecontroller/model/InvokeResponseData.java
new file mode 100644
index 0000000..2630515
--- /dev/null
+++ b/src/controller/java/src/chip/devicecontroller/model/InvokeResponseData.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (c) 2023 Project CHIP Authors
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package chip.devicecontroller.model;
+
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** Class for tracking invoke response data with either data or status */
+public final class InvokeResponseData {
+ private static final Logger logger = Logger.getLogger(InvokeResponseData.class.getName());
+ @Nullable private final ChipPathId endpointId;
+ private final ChipPathId clusterId, commandId;
+ private final Optional<Integer> commandRef;
+ @Nullable private final byte[] tlv;
+ @Nullable private final JSONObject json;
+ @Nullable private final Status status;
+
+ private InvokeResponseData(
+ ChipPathId endpointId,
+ ChipPathId clusterId,
+ ChipPathId commandId,
+ Optional<Integer> commandRef,
+ @Nullable byte[] tlv,
+ @Nullable String jsonString) {
+ this.endpointId = endpointId;
+ this.clusterId = clusterId;
+ this.commandId = commandId;
+ this.commandRef = commandRef;
+
+ if (tlv != null) {
+ this.tlv = tlv.clone();
+ } else {
+ this.tlv = null;
+ }
+
+ JSONObject jsonObject = null;
+ if (jsonString != null) {
+ try {
+ jsonObject = new JSONObject(jsonString);
+ } catch (JSONException ex) {
+ logger.log(Level.SEVERE, "Error parsing JSON string", ex);
+ }
+ }
+
+ this.json = jsonObject;
+ this.status = null;
+ }
+
+ private InvokeResponseData(
+ ChipPathId endpointId,
+ ChipPathId clusterId,
+ ChipPathId commandId,
+ Optional<Integer> commandRef,
+ int status,
+ Optional<Integer> clusterStatus) {
+ this.endpointId = endpointId;
+ this.clusterId = clusterId;
+ this.commandId = commandId;
+ this.commandRef = commandRef;
+ this.status = Status.newInstance(status, clusterStatus);
+ this.tlv = null;
+ this.json = null;
+ }
+
+ public ChipPathId getEndpointId() {
+ return endpointId;
+ }
+
+ public ChipPathId getClusterId() {
+ return clusterId;
+ }
+
+ public ChipPathId getCommandId() {
+ return commandId;
+ }
+
+ public Optional<Integer> getCommandRef() {
+ return commandRef;
+ }
+
+ @Nullable
+ public Status getStatus() {
+ return status;
+ }
+
+ // For use in JNI.
+ private long getEndpointId(long wildcardValue) {
+ return endpointId.getId(wildcardValue);
+ }
+
+ private long getClusterId(long wildcardValue) {
+ return clusterId.getId(wildcardValue);
+ }
+
+ private long getCommandId(long wildcardValue) {
+ return commandId.getId(wildcardValue);
+ }
+
+ public boolean isEndpointIdValid() {
+ return endpointId != null;
+ }
+
+ @Nullable
+ public byte[] getTlvByteArray() {
+ if (tlv != null) {
+ return tlv.clone();
+ }
+ return null;
+ }
+
+ @Nullable
+ public JSONObject getJsonObject() {
+ return json;
+ }
+
+ @Nullable
+ public String getJsonString() {
+ if (json == null) return null;
+ return json.toString();
+ }
+
+ // check whether the current InvokeResponseData has same path as others.
+ @Override
+ public boolean equals(Object object) {
+ if (object instanceof InvokeResponseData) {
+ InvokeResponseData that = (InvokeResponseData) object;
+ return Objects.equals(this.endpointId, that.endpointId)
+ && Objects.equals(this.clusterId, that.clusterId)
+ && Objects.equals(this.commandId, that.commandId);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(endpointId, clusterId, commandId);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.ENGLISH,
+ "Endpoint %s, cluster %s, command %s, payload: %s, status: %s",
+ endpointId,
+ clusterId,
+ commandId,
+ json == null ? "null" : getJsonString(),
+ status == null ? "null" : status.toString());
+ }
+
+ public static InvokeResponseData newInstance(
+ ChipPathId endpointId,
+ ChipPathId clusterId,
+ ChipPathId commandId,
+ Optional<Integer> commandRef,
+ @Nullable byte[] tlv,
+ @Nullable String jsonString) {
+ return new InvokeResponseData(endpointId, clusterId, commandId, commandRef, tlv, jsonString);
+ }
+
+ public static InvokeResponseData newInstance(
+ int endpointId,
+ long clusterId,
+ long commandId,
+ Optional<Integer> commandRef,
+ @Nullable byte[] tlv,
+ @Nullable String jsonString) {
+ return new InvokeResponseData(
+ ChipPathId.forId(endpointId),
+ ChipPathId.forId(clusterId),
+ ChipPathId.forId(commandId),
+ commandRef,
+ tlv,
+ jsonString);
+ }
+
+ public static InvokeResponseData newInstance(
+ ChipPathId endpointId,
+ ChipPathId clusterId,
+ ChipPathId commandId,
+ Optional<Integer> commandRef,
+ int status,
+ Optional<Integer> clusterStatus) {
+ return new InvokeResponseData(
+ endpointId, clusterId, commandId, commandRef, status, clusterStatus);
+ }
+
+ public static InvokeResponseData newInstance(
+ int endpointId,
+ long clusterId,
+ long commandId,
+ Optional<Integer> commandRef,
+ int status,
+ Optional<Integer> clusterStatus) {
+ return new InvokeResponseData(
+ ChipPathId.forId(endpointId),
+ ChipPathId.forId(clusterId),
+ ChipPathId.forId(commandId),
+ commandRef,
+ status,
+ clusterStatus);
+ }
+}
diff --git a/src/controller/java/src/chip/devicecontroller/model/NoInvokeResponseData.java b/src/controller/java/src/chip/devicecontroller/model/NoInvokeResponseData.java
new file mode 100644
index 0000000..03e930c
--- /dev/null
+++ b/src/controller/java/src/chip/devicecontroller/model/NoInvokeResponseData.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2024 Project CHIP Authors
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package chip.devicecontroller.model;
+
+import java.util.logging.Logger;
+
+/** Class for tracking failed invoke response data. */
+public final class NoInvokeResponseData {
+ private static final Logger logger = Logger.getLogger(NoInvokeResponseData.class.getName());
+ private final Integer commandRef;
+
+ private NoInvokeResponseData(int commandRef) {
+ this.commandRef = commandRef;
+ }
+
+ public Integer getCommandRef() {
+ return commandRef;
+ }
+
+ public static NoInvokeResponseData newInstance(int commandRef) {
+ return new NoInvokeResponseData(commandRef);
+ }
+}
diff --git a/src/controller/java/src/chip/devicecontroller/model/Status.java b/src/controller/java/src/chip/devicecontroller/model/Status.java
index d859fae..28abd36 100644
--- a/src/controller/java/src/chip/devicecontroller/model/Status.java
+++ b/src/controller/java/src/chip/devicecontroller/model/Status.java
@@ -124,4 +124,8 @@
public static Status newInstance(int status, Integer clusterStatus) {
return new Status(status, Optional.ofNullable(clusterStatus));
}
+
+ public static Status newInstance(int status, Optional<Integer> clusterStatus) {
+ return new Status(status, clusterStatus);
+ }
}
diff --git a/src/lib/core/core.gni b/src/lib/core/core.gni
index 8c7032e..02f98e3 100644
--- a/src/lib/core/core.gni
+++ b/src/lib/core/core.gni
@@ -100,7 +100,8 @@
chip_tlv_validate_char_string_on_read = false
chip_enable_sending_batch_commands =
- current_os == "linux" || current_os == "mac" || current_os == "ios"
+ current_os == "linux" || current_os == "mac" || current_os == "ios" ||
+ current_os == "android"
}
if (chip_target_style == "") {