From 300381788106947a136ca2c230270d423a16350f Mon Sep 17 00:00:00 2001 From: yunhanw-google Date: Tue, 7 Mar 2023 05:48:14 -0800 Subject: [PATCH] Add JAVA/JNI Invoke command API (#25476) * Add Java/JNI Invoke command API * address comments --- examples/java-matter-controller/BUILD.gn | 1 + .../java/src/com/matter/controller/Main.kt | 1 + .../PairOnNetworkLongImInvokeCommand.java | 91 ++++++++++ src/controller/java/AndroidCallbacks-JNI.cpp | 14 ++ src/controller/java/AndroidCallbacks.cpp | 156 ++++++++++++++++- src/controller/java/AndroidCallbacks.h | 23 +++ src/controller/java/BUILD.gn | 2 + .../java/CHIPDeviceController-JNI.cpp | 161 +++++++++++++++--- .../ChipDeviceController.java | 35 ++++ .../chip/devicecontroller/InvokeCallback.java | 49 ++++++ .../devicecontroller/InvokeCallbackJni.java | 48 ++++++ .../WriteAttributesCallbackJni.java | 2 +- .../devicecontroller/model/InvokeElement.java | 61 ++++++- 13 files changed, 610 insertions(+), 34 deletions(-) create mode 100644 examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImInvokeCommand.java create mode 100644 src/controller/java/src/chip/devicecontroller/InvokeCallback.java create mode 100644 src/controller/java/src/chip/devicecontroller/InvokeCallbackJni.java diff --git a/examples/java-matter-controller/BUILD.gn b/examples/java-matter-controller/BUILD.gn index 6314b71fc0f960..83aea5b0e000ff 100644 --- a/examples/java-matter-controller/BUILD.gn +++ b/examples/java-matter-controller/BUILD.gn @@ -51,6 +51,7 @@ java_library("java") { "java/src/com/matter/controller/commands/pairing/PairOnNetworkFabricCommand.java", "java/src/com/matter/controller/commands/pairing/PairOnNetworkInstanceNameCommand.java", "java/src/com/matter/controller/commands/pairing/PairOnNetworkLongCommand.java", + "java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImInvokeCommand.java", "java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImWriteCommand.java", "java/src/com/matter/controller/commands/pairing/PairOnNetworkShortCommand.java", "java/src/com/matter/controller/commands/pairing/PairOnNetworkVendorCommand.java", 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 34167a469cfc79..49cbea1ae65794 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 @@ -63,6 +63,7 @@ private fun getImCommands( ): List { return listOf( PairOnNetworkLongImWriteCommand(controller, credentialsIssuer), + PairOnNetworkLongImInvokeCommand(controller, credentialsIssuer), ) } diff --git a/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImInvokeCommand.java b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImInvokeCommand.java new file mode 100644 index 00000000000000..6195b8fff690fa --- /dev/null +++ b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImInvokeCommand.java @@ -0,0 +1,91 @@ +package com.matter.controller.commands.pairing; + +import chip.devicecontroller.ChipDeviceController; +import chip.devicecontroller.GetConnectedDeviceCallbackJni.GetConnectedDeviceCallback; +import chip.devicecontroller.InvokeCallback; +import chip.devicecontroller.model.InvokeElement; +import com.matter.controller.commands.common.CredentialsIssuer; +import java.util.logging.Level; +import java.util.logging.Logger; + +public final class PairOnNetworkLongImInvokeCommand extends PairingCommand { + private static final Logger logger = + Logger.getLogger(PairOnNetworkLongImInvokeCommand.class.getName()); + private static final int MATTER_PORT = 5540; + private static final int CLUSTER_ID_IDENTIFY = 0x0003; + private static final int IDENTIFY_COMMAND = 0; + private long devicePointer; + + private void setDevicePointer(long devicePointer) { + this.devicePointer = devicePointer; + } + + private class InternalInvokeCallback implements InvokeCallback { + @Override + public void onError(Exception e) { + logger.log(Level.INFO, "Invoke receive onError" + e.getMessage()); + setFailure("write failure"); + } + + @Override + public void onResponse(InvokeElement element, long successCode) { + logger.log(Level.INFO, "Invoke receive OnResponse on "); + if (element != null) { + logger.log(Level.INFO, element.toString()); + } + logger.log(Level.INFO, "success code is" + String.valueOf(successCode)); + setSuccess(); + } + } + + private class InternalGetConnectedDeviceCallback implements GetConnectedDeviceCallback { + @Override + public void onDeviceConnected(long devicePointer) { + setDevicePointer(devicePointer); + logger.log(Level.INFO, "onDeviceConnected"); + } + + @Override + public void onConnectionFailure(long nodeId, Exception error) { + logger.log(Level.INFO, "onConnectionFailure"); + } + } + + public PairOnNetworkLongImInvokeCommand( + ChipDeviceController controller, CredentialsIssuer credsIssue) { + super( + controller, + "onnetwork-long-im-invoke", + PairingModeType.ON_NETWORK, + PairingNetworkType.NONE, + credsIssue, + DiscoveryFilterType.LONG_DISCRIMINATOR); + } + + @Override + protected void runCommand() { + // tlv structure with tag 0, unsigned integer 1 inside, {0: 1} + byte[] intTLV = {0x15, 0x24, 0x00, 0x01, 0x18}; + InvokeElement element = + InvokeElement.newInstance( + /* endpointId= */ 0, CLUSTER_ID_IDENTIFY, IDENTIFY_COMMAND, intTLV, null); + + currentCommissioner() + .pairDeviceWithAddress( + getNodeId(), + getRemoteAddr().getHostAddress(), + MATTER_PORT, + getDiscriminator(), + getSetupPINCode(), + null); + currentCommissioner().setCompletionListener(this); + waitCompleteMs(getTimeoutMillis()); + currentCommissioner() + .getConnectedDevicePointer(getNodeId(), new InternalGetConnectedDeviceCallback()); + clear(); + + currentCommissioner().invoke(new InternalInvokeCallback(), devicePointer, element, 0, 0); + + waitCompleteMs(getTimeoutMillis()); + } +} diff --git a/src/controller/java/AndroidCallbacks-JNI.cpp b/src/controller/java/AndroidCallbacks-JNI.cpp index c3f1dbeaee675d..95e9859bdeb701 100644 --- a/src/controller/java/AndroidCallbacks-JNI.cpp +++ b/src/controller/java/AndroidCallbacks-JNI.cpp @@ -85,3 +85,17 @@ JNI_METHOD(void, WriteAttributesCallbackJni, deleteCallback)(JNIEnv * env, jobje VerifyOrReturn(writeAttributesCallback != nullptr, ChipLogError(Controller, "WriteAttributesCallback handle is nullptr")); delete writeAttributesCallback; } + +JNI_METHOD(jlong, InvokeCallbackJni, newCallback) +(JNIEnv * env, jobject self, jobject invokeCallbackJava) +{ + InvokeCallback * invokeCallback = chip::Platform::New(self, invokeCallbackJava); + return reinterpret_cast(invokeCallback); +} + +JNI_METHOD(void, InvokeCallbackJni, deleteCallback)(JNIEnv * env, jobject self, jlong callbackHandle) +{ + InvokeCallback * invokeCallback = reinterpret_cast(callbackHandle); + VerifyOrReturn(invokeCallback != nullptr, ChipLogError(Controller, "InvokeCallback handle is nullptr")); + delete invokeCallback; +} diff --git a/src/controller/java/AndroidCallbacks.cpp b/src/controller/java/AndroidCallbacks.cpp index 573ed47a4535b9..f8b113a768b0d1 100644 --- a/src/controller/java/AndroidCallbacks.cpp +++ b/src/controller/java/AndroidCallbacks.cpp @@ -422,6 +422,61 @@ CHIP_ERROR CreateChipAttributePath(const app::ConcreteDataAttributePath & aPath, return err; } +CHIP_ERROR InvokeCallback::CreateInvokeElement(const app::ConcreteCommandPath & aPath, TLV::TLVReader * apData, jobject & outObj) +{ + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + CHIP_ERROR err = CHIP_NO_ERROR; + + jclass invokeElementCls = nullptr; + err = JniReferences::GetInstance().GetClassRef(env, "chip/devicecontroller/model/InvokeElement", invokeElementCls); + ReturnErrorOnFailure(err); + JniClass invokeElementJniCls(invokeElementCls); + + jmethodID invokeElementCtor = env->GetStaticMethodID(invokeElementCls, "newInstance", + "(JJJ[BLjava/lang/String;)Lchip/devicecontroller/model/InvokeElement;"); + VerifyOrReturnError(!env->ExceptionCheck(), CHIP_JNI_ERROR_EXCEPTION_THROWN); + VerifyOrReturnError(invokeElementCtor != nullptr, CHIP_JNI_ERROR_METHOD_NOT_FOUND); + + if (apData != nullptr) + { + TLV::TLVReader readerForJavaTLV; + TLV::TLVReader readerForJson; + readerForJavaTLV.Init(*apData); + readerForJson.Init(*apData); + + // Create TLV byte array to pass to Java layer + size_t bufferLen = readerForJavaTLV.GetRemainingLength() + readerForJavaTLV.GetLengthRead(); + std::unique_ptr buffer = std::unique_ptr(new uint8_t[bufferLen]); + uint32_t size = 0; + // The TLVReader's read head is not pointing to the first element in the container, instead of the container itself, use + // a TLVWriter to get a TLV with a normalized TLV buffer (Wrapped with an anonymous tag, no extra "end of container" tag + // at the end.) + TLV::TLVWriter writer; + writer.Init(buffer.get(), bufferLen); + err = writer.CopyElement(TLV::AnonymousTag(), readerForJavaTLV); + ReturnErrorOnFailure(err); + size = writer.GetLengthWritten(); + chip::ByteArray jniByteArray(env, reinterpret_cast(buffer.get()), size); + + // Convert TLV to JSON + Json::Value json; + err = TlvToJson(readerForJson, json); + ReturnErrorOnFailure(err); + + UtfString jsonString(env, JsonToString(json).c_str()); + outObj = env->CallStaticObjectMethod(invokeElementCls, invokeElementCtor, aPath.mEndpointId, aPath.mClusterId, + aPath.mCommandId, jniByteArray.jniValue(), jsonString.jniValue()); + } + else + { + outObj = env->CallStaticObjectMethod(invokeElementCls, invokeElementCtor, aPath.mEndpointId, aPath.mClusterId, + aPath.mCommandId, nullptr, nullptr); + } + VerifyOrReturnError(outObj != nullptr, CHIP_JNI_ERROR_NULL_OBJECT); + + return err; +} + CHIP_ERROR ReportCallback::CreateChipEventPath(const app::ConcreteEventPath & aPath, jobject & outObj) { JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); @@ -856,7 +911,7 @@ void WriteAttributesCallback::ReportError(jobject attributePath, const char * me CHIP_ERROR err = CHIP_NO_ERROR; JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); - ChipLogError(Controller, "ReportError is called"); + ChipLogError(Controller, "WriteAttributesCallback ReportError is called"); jthrowable exception; err = AndroidClusterExceptions::GetInstance().CreateIllegalStateException(env, message, errorCode, exception); VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Unable to create IllegalStateException: %s", ErrorStr(err))); @@ -872,5 +927,104 @@ void WriteAttributesCallback::ReportError(jobject attributePath, const char * me VerifyOrReturn(!env->ExceptionCheck(), env->ExceptionDescribe()); } +InvokeCallback::InvokeCallback(jobject wrapperCallback, jobject javaCallback) +{ + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturn(env != nullptr, ChipLogError(Controller, "Could not get JNIEnv for current thread")); + + mWrapperCallbackRef = env->NewGlobalRef(wrapperCallback); + VerifyOrReturn(!env->ExceptionCheck(), env->ExceptionDescribe()); + if (mWrapperCallbackRef == nullptr) + { + ChipLogError(Controller, "Could not create global reference for Wrapper InvokeCallback"); + } + mJavaCallbackRef = env->NewGlobalRef(javaCallback); + VerifyOrReturn(!env->ExceptionCheck(), env->ExceptionDescribe()); + if (mJavaCallbackRef == nullptr) + { + ChipLogError(Controller, "Could not create global reference for Java InvokeCallback"); + } +} + +InvokeCallback::~InvokeCallback() +{ + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturn(env != nullptr, ChipLogError(Controller, "Could not get JNIEnv for current thread")); + env->DeleteGlobalRef(mJavaCallbackRef); + if (mCommandSender != nullptr) + { + Platform::Delete(mCommandSender); + } +} + +void InvokeCallback::OnResponse(app::CommandSender * apCommandSender, const app::ConcreteCommandPath & aPath, + const app::StatusIB & aStatusIB, TLV::TLVReader * apData) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + jobject invokeElementObj = nullptr; + jmethodID onResponseMethod; + + err = CreateInvokeElement(aPath, apData, invokeElementObj); + VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Unable to create Java InvokeElement: %s", ErrorStr(err))); + err = JniReferences::GetInstance().FindMethod(env, mJavaCallbackRef, "onResponse", + "(Lchip/devicecontroller/model/InvokeElement;J)V", &onResponseMethod); + VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Unable to find onError method: %s", ErrorStr(err))); + + DeviceLayer::StackUnlock unlock; + env->CallVoidMethod(mJavaCallbackRef, onResponseMethod, invokeElementObj, + static_cast>(aStatusIB.mStatus)); + VerifyOrReturn(!env->ExceptionCheck(), env->ExceptionDescribe()); +} + +void InvokeCallback::OnError(const app::CommandSender * apCommandSender, CHIP_ERROR aError) +{ + ReportError(aError); +} + +void InvokeCallback::OnDone(app::CommandSender * apCommandSender) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + + jmethodID onDoneMethod; + err = JniReferences::GetInstance().FindMethod(env, mJavaCallbackRef, "onDone", "()V", &onDoneMethod); + VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Could not find onDone method")); + + DeviceLayer::StackUnlock unlock; + env->CallVoidMethod(mJavaCallbackRef, onDoneMethod); + VerifyOrReturn(!env->ExceptionCheck(), env->ExceptionDescribe()); + JniReferences::GetInstance().GetEnvForCurrentThread()->DeleteGlobalRef(mWrapperCallbackRef); +} + +void InvokeCallback::ReportError(CHIP_ERROR err) +{ + ReportError(ErrorStr(err), err.AsInteger()); +} + +void InvokeCallback::ReportError(Protocols::InteractionModel::Status status) +{ + ReportError("IM Status", static_cast>(status)); +} + +void InvokeCallback::ReportError(const char * message, ChipError::StorageType errorCode) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + + ChipLogError(Controller, "InvokeCallback ReportError is called"); + jthrowable exception; + err = AndroidClusterExceptions::GetInstance().CreateIllegalStateException(env, message, errorCode, exception); + VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Unable to create IllegalStateException: %s", ErrorStr(err))); + + jmethodID onErrorMethod; + err = JniReferences::GetInstance().FindMethod(env, mJavaCallbackRef, "onError", "(Ljava/lang/Exception;)V", &onErrorMethod); + VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Unable to find onError method: %s", ErrorStr(err))); + + DeviceLayer::StackUnlock unlock; + env->CallVoidMethod(mJavaCallbackRef, onErrorMethod, exception); + VerifyOrReturn(!env->ExceptionCheck(), env->ExceptionDescribe()); +} + } // namespace Controller } // namespace chip diff --git a/src/controller/java/AndroidCallbacks.h b/src/controller/java/AndroidCallbacks.h index 8d6c44e7c5a55d..cd058eae43fe56 100644 --- a/src/controller/java/AndroidCallbacks.h +++ b/src/controller/java/AndroidCallbacks.h @@ -17,6 +17,7 @@ #pragma once #include +#include #include #include #include @@ -152,5 +153,27 @@ struct WriteAttributesCallback : public app::WriteClient::Callback jobject mJavaCallbackRef = nullptr; }; +struct InvokeCallback : public app::CommandSender::Callback +{ + InvokeCallback(jobject wrapperCallback, jobject javaCallback); + ~InvokeCallback(); + + void OnResponse(app::CommandSender * apCommandSender, const app::ConcreteCommandPath & aPath, const app::StatusIB & aStatusIB, + TLV::TLVReader * apData) override; + /** Report errors back to Java layer. attributePath may be nullptr for general errors. */ + void OnError(const app::CommandSender * apCommandSender, CHIP_ERROR aError) override; + + void OnDone(app::CommandSender * apCommandSender) override; + + CHIP_ERROR CreateInvokeElement(const app::ConcreteCommandPath & aPath, TLV::TLVReader * apData, jobject & outObj); + void ReportError(CHIP_ERROR err); + void ReportError(Protocols::InteractionModel::Status status); + void ReportError(const char * message, ChipError::StorageType errorCode); + + app::CommandSender * mCommandSender = nullptr; + jobject mWrapperCallbackRef = nullptr; + jobject mJavaCallbackRef = nullptr; +}; + } // namespace Controller } // namespace chip diff --git a/src/controller/java/BUILD.gn b/src/controller/java/BUILD.gn index 9e2faa6ef2a9ce..6cbdb46912c92e 100644 --- a/src/controller/java/BUILD.gn +++ b/src/controller/java/BUILD.gn @@ -183,6 +183,8 @@ android_library("java") { "src/chip/devicecontroller/DeviceAttestationDelegate.java", "src/chip/devicecontroller/DiscoveredDevice.java", "src/chip/devicecontroller/GetConnectedDeviceCallbackJni.java", + "src/chip/devicecontroller/InvokeCallback.java", + "src/chip/devicecontroller/InvokeCallbackJni.java", "src/chip/devicecontroller/KeypairDelegate.java", "src/chip/devicecontroller/NetworkCredentials.java", "src/chip/devicecontroller/NetworkLocation.java", diff --git a/src/controller/java/CHIPDeviceController-JNI.cpp b/src/controller/java/CHIPDeviceController-JNI.cpp index 1c15204c6f394a..566832d59e5020 100644 --- a/src/controller/java/CHIPDeviceController-JNI.cpp +++ b/src/controller/java/CHIPDeviceController-JNI.cpp @@ -1401,17 +1401,20 @@ JNI_METHOD(void, write) jsize length = 0; TLV::TLVReader reader; - SuccessOrExit(JniReferences::GetInstance().GetListItem(attributeList, i, attributeItem)); - SuccessOrExit(JniReferences::GetInstance().FindMethod(env, attributeItem, "getEndpointId", - "()Lchip/devicecontroller/model/ChipPathId;", &getEndpointIdMethod)); - SuccessOrExit(JniReferences::GetInstance().FindMethod(env, attributeItem, "getClusterId", - "()Lchip/devicecontroller/model/ChipPathId;", &getClusterIdMethod)); - SuccessOrExit(JniReferences::GetInstance().FindMethod(env, attributeItem, "getAttributeId", - "()Lchip/devicecontroller/model/ChipPathId;", &getAttributeIdMethod)); - SuccessOrExit(JniReferences::GetInstance().FindMethod(env, attributeItem, "hasDataVersion", "()Z", &hasDataVersionMethod)); - SuccessOrExit(JniReferences::GetInstance().FindMethod(env, attributeItem, "getDataVersion", "()I", &getDataVersionMethod)); + SuccessOrExit(err = JniReferences::GetInstance().GetListItem(attributeList, i, attributeItem)); + SuccessOrExit(err = JniReferences::GetInstance().FindMethod( + env, attributeItem, "getEndpointId", "()Lchip/devicecontroller/model/ChipPathId;", &getEndpointIdMethod)); + SuccessOrExit(err = JniReferences::GetInstance().FindMethod( + env, attributeItem, "getClusterId", "()Lchip/devicecontroller/model/ChipPathId;", &getClusterIdMethod)); + SuccessOrExit(err = JniReferences::GetInstance().FindMethod(env, attributeItem, "getAttributeId", + "()Lchip/devicecontroller/model/ChipPathId;", + &getAttributeIdMethod)); SuccessOrExit( - JniReferences::GetInstance().FindMethod(env, attributeItem, "getTlvByteArray", "()[B", &getTlvByteArrayMethod)); + err = JniReferences::GetInstance().FindMethod(env, attributeItem, "hasDataVersion", "()Z", &hasDataVersionMethod)); + SuccessOrExit( + err = JniReferences::GetInstance().FindMethod(env, attributeItem, "getDataVersion", "()I", &getDataVersionMethod)); + SuccessOrExit( + err = JniReferences::GetInstance().FindMethod(env, attributeItem, "getTlvByteArray", "()[B", &getTlvByteArrayMethod)); endpointIdObj = env->CallObjectMethod(attributeItem, getEndpointIdMethod); VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN); @@ -1425,9 +1428,9 @@ JNI_METHOD(void, write) VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN); VerifyOrExit(attributeIdObj != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT); - SuccessOrExit(GetChipPathIdValue(endpointIdObj, kInvalidEndpointId, endpointId)); - SuccessOrExit(GetChipPathIdValue(clusterIdObj, kInvalidClusterId, clusterId)); - SuccessOrExit(GetChipPathIdValue(attributeIdObj, kInvalidAttributeId, attributeId)); + SuccessOrExit(err = GetChipPathIdValue(endpointIdObj, kInvalidEndpointId, endpointId)); + SuccessOrExit(err = GetChipPathIdValue(clusterIdObj, kInvalidClusterId, clusterId)); + SuccessOrExit(err = GetChipPathIdValue(attributeIdObj, kInvalidAttributeId, attributeId)); hasDataVersion = static_cast(env->CallBooleanMethod(attributeItem, hasDataVersionMethod)); VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN); @@ -1458,26 +1461,136 @@ JNI_METHOD(void, write) err = writeClient->SendWriteRequest(device->GetSecureSession().Value(), imTimeoutMs != 0 ? System::Clock::Milliseconds32(imTimeoutMs) : System::Clock::kZero); + SuccessOrExit(err); + callback->mWriteClient = writeClient; + +exit: + if (err != CHIP_NO_ERROR) { + ChipLogError(Controller, "JNI IM Write Error: %s", err.AsString()); + if (err == CHIP_JNI_ERROR_EXCEPTION_THROWN) + { + env->ExceptionDescribe(); + env->ExceptionClear(); + } callback->OnError(writeClient, err); - delete writeClient; - delete callback; - return; + if (writeClient != nullptr) + { + delete writeClient; + } + if (callback != nullptr) + { + delete callback; + } } +} - callback->mWriteClient = writeClient; +JNI_METHOD(void, invoke) +(JNIEnv * env, jobject self, jlong handle, jlong callbackHandle, jlong devicePtr, jobject invokeElement, jint timedRequestTimeoutMs, + jint imTimeoutMs) +{ + chip::DeviceLayer::StackLock lock; + CHIP_ERROR err = CHIP_NO_ERROR; + auto callback = reinterpret_cast(callbackHandle); + app::CommandSender * commandSender; + uint32_t endpointId = 0; + uint32_t clusterId = 0; + uint32_t commandId = 0; + jmethodID getEndpointIdMethod = nullptr; + jmethodID getClusterIdMethod = nullptr; + jmethodID getCommandIdMethod = nullptr; + jmethodID getTlvByteArrayMethod = nullptr; + jobject endpointIdObj = nullptr; + jobject clusterIdObj = nullptr; + jobject commandIdObj = nullptr; + jbyteArray tlvBytesObj = nullptr; + jbyte * tlvBytesObjBytes = nullptr; + jsize length = 0; + TLV::TLVReader reader; + TLV::TLVWriter * writer = nullptr; + + ChipLogDetail(Controller, "IM invoke() called"); + + DeviceProxy * device = reinterpret_cast(devicePtr); + VerifyOrExit(device != nullptr, err = CHIP_ERROR_INCORRECT_STATE); + VerifyOrExit(device->GetSecureSession().HasValue(), err = CHIP_ERROR_MISSING_SECURE_SESSION); + + commandSender = Platform::New(callback, device->GetExchangeManager(), timedRequestTimeoutMs != 0); + + SuccessOrExit(err = JniReferences::GetInstance().FindMethod( + env, invokeElement, "getEndpointId", "()Lchip/devicecontroller/model/ChipPathId;", &getEndpointIdMethod)); + SuccessOrExit(err = JniReferences::GetInstance().FindMethod(env, invokeElement, "getClusterId", + "()Lchip/devicecontroller/model/ChipPathId;", &getClusterIdMethod)); + SuccessOrExit(err = JniReferences::GetInstance().FindMethod(env, invokeElement, "getCommandId", + "()Lchip/devicecontroller/model/ChipPathId;", &getCommandIdMethod)); + SuccessOrExit(JniReferences::GetInstance().FindMethod(env, invokeElement, "getTlvByteArray", "()[B", &getTlvByteArrayMethod)); + + endpointIdObj = env->CallObjectMethod(invokeElement, getEndpointIdMethod); + VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN); + VerifyOrExit(endpointIdObj != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT); + + clusterIdObj = env->CallObjectMethod(invokeElement, getClusterIdMethod); + VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN); + VerifyOrExit(clusterIdObj != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT); + + commandIdObj = env->CallObjectMethod(invokeElement, getCommandIdMethod); + VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN); + VerifyOrExit(commandIdObj != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT); + + SuccessOrExit(err = GetChipPathIdValue(endpointIdObj, kInvalidEndpointId, endpointId)); + SuccessOrExit(err = GetChipPathIdValue(clusterIdObj, kInvalidClusterId, clusterId)); + SuccessOrExit(err = GetChipPathIdValue(commandIdObj, kInvalidCommandId, commandId)); + + tlvBytesObj = static_cast(env->CallObjectMethod(invokeElement, getTlvByteArrayMethod)); + VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN); + VerifyOrExit(tlvBytesObj != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT); + + tlvBytesObjBytes = env->GetByteArrayElements(tlvBytesObj, nullptr); + VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN); + length = env->GetArrayLength(tlvBytesObj); + VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN); + SuccessOrExit(err = commandSender->PrepareCommand(app::CommandPathParams(static_cast(endpointId), /* group id */ 0, + static_cast(clusterId), + static_cast(commandId), + app::CommandPathFlags::kEndpointIdValid), + false)); + + writer = commandSender->GetCommandDataIBTLVWriter(); + VerifyOrExit(writer != nullptr, err = CHIP_ERROR_INCORRECT_STATE); + reader.Init(reinterpret_cast(tlvBytesObjBytes), static_cast(length)); + reader.Next(); + SuccessOrExit(err = writer->CopyContainer(TLV::ContextTag(app::CommandDataIB::Tag::kFields), reader)); + + SuccessOrExit(err = commandSender->FinishCommand(timedRequestTimeoutMs != 0 ? Optional(timedRequestTimeoutMs) + : Optional::Missing())); + + SuccessOrExit(err = + commandSender->SendCommandRequest(device->GetSecureSession().Value(), + imTimeoutMs != 0 ? MakeOptional(System::Clock::Milliseconds32(imTimeoutMs)) + : Optional::Missing())); + + callback->mCommandSender = commandSender; exit: - if (err == CHIP_JNI_ERROR_EXCEPTION_THROWN) - { - ChipLogError(DeviceLayer, "Java exception in IM Write JNI"); - env->ExceptionDescribe(); - env->ExceptionClear(); - } + if (err != CHIP_NO_ERROR) { - JniReferences::GetInstance().ThrowError(env, sChipDeviceControllerExceptionCls, err); + ChipLogError(Controller, "JNI IM Invoke Error: %s", err.AsString()); + if (err == CHIP_JNI_ERROR_EXCEPTION_THROWN) + { + env->ExceptionDescribe(); + env->ExceptionClear(); + } + callback->OnError(nullptr, err); + if (commandSender != nullptr) + { + delete commandSender; + } + if (callback != nullptr) + { + delete callback; + } } } diff --git a/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java b/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java index fbe2f172d8786c..15e3ea64ae2644 100644 --- a/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java +++ b/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java @@ -23,6 +23,7 @@ import chip.devicecontroller.model.AttributeWriteRequest; import chip.devicecontroller.model.ChipAttributePath; import chip.devicecontroller.model.ChipEventPath; +import chip.devicecontroller.model.InvokeElement; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -626,6 +627,32 @@ public void write( imTimeoutMs); } + /** + * @brief Invoke command to target device + * @param InvokeCallback Callback when an invoke response has been received and processed for the + * given invoke command. + * @param devicePtr connected device pointer + * @param invokeElement invoke command's path and arguments + * @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 invoke( + InvokeCallback callback, + long devicePtr, + InvokeElement invokeElement, + int timedRequestTimeoutMs, + int imTimeoutMs) { + InvokeCallbackJni jniCallback = new InvokeCallbackJni(callback); + invoke( + deviceControllerPtr, + jniCallback.getCallbackHandle(), + devicePtr, + invokeElement, + timedRequestTimeoutMs, + imTimeoutMs); + } + /** * Converts a given X.509v3 certificate into a Matter certificate. * @@ -681,6 +708,14 @@ private native void write( int timedRequestTimeoutMs, int imTimeoutMs); + private native void invoke( + long deviceControllerPtr, + long callbackHandle, + long devicePtr, + InvokeElement invokeElement, + int timedRequestTimeoutMs, + int imTimeoutMs); + private native long newDeviceController(ControllerParams params); private native void setDeviceAttestationDelegate( diff --git a/src/controller/java/src/chip/devicecontroller/InvokeCallback.java b/src/controller/java/src/chip/devicecontroller/InvokeCallback.java new file mode 100644 index 00000000000000..0d4fccda025b0d --- /dev/null +++ b/src/controller/java/src/chip/devicecontroller/InvokeCallback.java @@ -0,0 +1,49 @@ +/* + * 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; + +import chip.devicecontroller.model.InvokeElement; + +/** An interface for receiving invoke response. */ +public interface InvokeCallback { + + /** + * 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 invoke response has been received and processed for the given + * path. + * + * @param invokeElement The invoke element in invoke response that could have null or nonNull tlv + * data + * @param successCode If data in InvokeElment is null, successCode can be any success status, + * including possibly a cluster-specific one. If data in InvokeElement is not null, + * successCode will always be a generic SUCCESS(0) with no-cluster specific information + */ + void onResponse(InvokeElement invokeElement, long successCode); + + default void onDone() {} +} diff --git a/src/controller/java/src/chip/devicecontroller/InvokeCallbackJni.java b/src/controller/java/src/chip/devicecontroller/InvokeCallbackJni.java new file mode 100644 index 00000000000000..926aa0088c21bb --- /dev/null +++ b/src/controller/java/src/chip/devicecontroller/InvokeCallbackJni.java @@ -0,0 +1,48 @@ +/* + * 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; + +/** JNI wrapper callback class for {@link InvokeCallback}. */ +public final class InvokeCallbackJni { + private final InvokeCallback wrappedInvokeCallback; + private long callbackHandle; + + public InvokeCallbackJni(InvokeCallback wrappedInvokeCallback) { + this.wrappedInvokeCallback = wrappedInvokeCallback; + this.callbackHandle = newCallback(wrappedInvokeCallback); + } + + long getCallbackHandle() { + return callbackHandle; + } + + private native long newCallback(InvokeCallback wrappedCallback); + + private native void deleteCallback(long callbackHandle); + + // 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/WriteAttributesCallbackJni.java b/src/controller/java/src/chip/devicecontroller/WriteAttributesCallbackJni.java index fb7d7136ed0572..38f98c3201103a 100644 --- a/src/controller/java/src/chip/devicecontroller/WriteAttributesCallbackJni.java +++ b/src/controller/java/src/chip/devicecontroller/WriteAttributesCallbackJni.java @@ -18,7 +18,7 @@ package chip.devicecontroller; /** JNI wrapper callback class for {@link WriteAttributesCallback}. */ -public class WriteAttributesCallbackJni { +public final class WriteAttributesCallbackJni { private final WriteAttributesCallback wrappedWriteAttributesCallback; private long callbackHandle; diff --git a/src/controller/java/src/chip/devicecontroller/model/InvokeElement.java b/src/controller/java/src/chip/devicecontroller/model/InvokeElement.java index c14a1b4a9f57b6..413d5ac8f4e1dc 100644 --- a/src/controller/java/src/chip/devicecontroller/model/InvokeElement.java +++ b/src/controller/java/src/chip/devicecontroller/model/InvokeElement.java @@ -19,18 +19,45 @@ import java.util.Locale; import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import org.json.JSONException; +import org.json.JSONObject; /** An invoke element that should be used for interaction model invoke request and response. */ public final class InvokeElement { + private static final Logger logger = Logger.getLogger(InvokeElement.class.getName()); private final ChipPathId endpointId, clusterId, commandId; - private final byte[] tlv; + @Nullable private final byte[] tlv; + @Nullable private final JSONObject json; private InvokeElement( - ChipPathId endpointId, ChipPathId clusterId, ChipPathId commandId, byte[] tlv) { + ChipPathId endpointId, + ChipPathId clusterId, + ChipPathId commandId, + @Nullable byte[] tlv, + @Nullable String jsonString) { this.endpointId = endpointId; this.clusterId = clusterId; this.commandId = commandId; - this.tlv = tlv.clone(); + + 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; } public ChipPathId getEndpointId() { @@ -45,8 +72,17 @@ public ChipPathId getCommandId() { return commandId; } + @Nullable public byte[] getTlvByteArray() { - return tlv.clone(); + if (tlv != null) { + return tlv.clone(); + } + return null; + } + + @Nullable + public JSONObject getJson() { + return json; } // check whether the current InvokeElement has same path as others. @@ -73,17 +109,26 @@ public String toString() { } public static InvokeElement newInstance( - ChipPathId endpointId, ChipPathId clusterId, ChipPathId commandId, byte[] tlv) { - return new InvokeElement(endpointId, clusterId, commandId, tlv); + ChipPathId endpointId, + ChipPathId clusterId, + ChipPathId commandId, + @Nullable byte[] tlv, + @Nullable String jsonString) { + return new InvokeElement(endpointId, clusterId, commandId, tlv, jsonString); } /** Create a new {@link InvokeElement} with only concrete ids. */ public static InvokeElement newInstance( - long endpointId, long clusterId, long commandId, byte[] tlv) { + long endpointId, + long clusterId, + long commandId, + @Nullable byte[] tlv, + @Nullable String jsonString) { return new InvokeElement( ChipPathId.forId(endpointId), ChipPathId.forId(clusterId), ChipPathId.forId(commandId), - tlv); + tlv, + jsonString); } }