From e0e8fa7b1a8b3bfc7d965c858c3f52eb53d8251c Mon Sep 17 00:00:00 2001 From: yunhanw Date: Sun, 5 Mar 2023 15:31:35 -0800 Subject: [PATCH] Add Java/JNI Invoke command API --- examples/java-matter-controller/BUILD.gn | 1 + .../java/src/com/matter/controller/Main.kt | 1 + .../PairOnNetworkLongImInvokeCommand.java | 90 +++++++++ src/controller/java/AndroidCallbacks-JNI.cpp | 14 ++ src/controller/java/AndroidCallbacks.cpp | 179 +++++++++++++++++- src/controller/java/AndroidCallbacks.h | 24 +++ src/controller/java/BUILD.gn | 2 + .../java/CHIPDeviceController-JNI.cpp | 168 +++++++++++++--- .../ChipDeviceController.java | 35 ++++ .../chip/devicecontroller/InvokeCallback.java | 48 +++++ .../devicecontroller/InvokeCallbackJni.java | 48 +++++ .../devicecontroller/model/InvokeElement.java | 105 +++++++++- 12 files changed, 684 insertions(+), 31 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..e664d0f6117e35 --- /dev/null +++ b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImInvokeCommand.java @@ -0,0 +1,90 @@ +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 int MATTER_PORT = 5540; + private long devicePointer; + private static final int CLUSTER_ID_IDENTIFY = 0x0003; + private static final int IDENTIFY_COMMAND = 0; + private static Logger logger = Logger.getLogger(PairOnNetworkLongImInvokeCommand.class.getName()); + + 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); + + 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..f20ef1900c2551 100644 --- a/src/controller/java/AndroidCallbacks.cpp +++ b/src/controller/java/AndroidCallbacks.cpp @@ -422,6 +422,76 @@ CHIP_ERROR CreateChipAttributePath(const app::ConcreteDataAttributePath & aPath, return err; } +CHIP_ERROR InvokeCallback::CreateInvokeElement(const app::ConcreteCommandPath & aPath, 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)Lchip/devicecontroller/model/InvokeElement;"); + VerifyOrReturnError(!env->ExceptionCheck(), CHIP_JNI_ERROR_EXCEPTION_THROWN); + VerifyOrReturnError(invokeElementCtor != nullptr, CHIP_JNI_ERROR_METHOD_NOT_FOUND); + + outObj = + env->CallStaticObjectMethod(invokeElementCls, invokeElementCtor, aPath.mEndpointId, aPath.mClusterId, aPath.mCommandId); + VerifyOrReturnError(outObj != nullptr, CHIP_JNI_ERROR_NULL_OBJECT); + + 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); + + 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()); + 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 +926,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 +942,112 @@ 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; + + if (apData != nullptr) + { + err = CreateInvokeElement(aPath, apData, invokeElementObj); + } + else + { + err = CreateInvokeElement(aPath, 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..37a4d536eca54a 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,28 @@ 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, jobject & outObj); + 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..f448aac62eb7df 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,143 @@ 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 hasTlvMethod = 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; + bool hasTlv = false; + + 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(err = JniReferences::GetInstance().FindMethod(env, invokeElement, "hasTlv", "()Z", &hasTlvMethod)); + 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)); + + hasTlv = static_cast(env->CallBooleanMethod(invokeElement, hasTlvMethod)); + VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN); + VerifyOrExit(hasTlv, err = CHIP_ERROR_INVALID_ARGUMENT); + + 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..e9e72de8f40b77 --- /dev/null +++ b/src/controller/java/src/chip/devicecontroller/InvokeCallback.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; + +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 when successCode is 0, tlvData is null, when successCode is not 0, tlvData + * is not null + */ + 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..a287fdabb5131f --- /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 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/model/InvokeElement.java b/src/controller/java/src/chip/devicecontroller/model/InvokeElement.java index c14a1b4a9f57b6..23fb208eecaeab 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.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +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 final ChipPathId endpointId, clusterId, commandId; - private final byte[] tlv; + private final Optional tlv; + private final Optional json; + private static Logger logger = Logger.getLogger(InvokeElement.class.getName()); private InvokeElement( - ChipPathId endpointId, ChipPathId clusterId, ChipPathId commandId, byte[] tlv) { + ChipPathId endpointId, + ChipPathId clusterId, + ChipPathId commandId, + Optional tlv, + Optional jsonString) { this.endpointId = endpointId; this.clusterId = clusterId; this.commandId = commandId; - this.tlv = tlv.clone(); + if (tlv.isPresent()) { + this.tlv = Optional.of(tlv.get().clone()); + } else { + this.tlv = Optional.empty(); + } + + JSONObject jsonObject = null; + if (jsonString.isPresent()) { + try { + jsonObject = new JSONObject(jsonString.get()); + } catch (JSONException ex) { + logger.log(Level.SEVERE, "Error parsing JSON string", ex); + } + } + + this.json = Optional.ofNullable(jsonObject); + ; } public ChipPathId getEndpointId() { @@ -45,8 +72,20 @@ public ChipPathId getCommandId() { return commandId; } + public boolean hasTlv() { + return tlv.isPresent(); + } + + public boolean hasJson() { + return json.isPresent(); + } + public byte[] getTlvByteArray() { - return tlv.clone(); + return tlv.get().clone(); + } + + public JSONObject getJson() { + return json.get(); } // check whether the current InvokeElement has same path as others. @@ -72,9 +111,30 @@ public String toString() { Locale.ENGLISH, "Endpoint %s, cluster %s, command %s", endpointId, clusterId, commandId); } + public static InvokeElement newInstance( + ChipPathId endpointId, + ChipPathId clusterId, + ChipPathId commandId, + byte[] tlv, + String jsonString) { + return new InvokeElement( + endpointId, clusterId, commandId, Optional.of(tlv), Optional.of(jsonString)); + } + + /** Create a new {@link InvokeElement} with only concrete ids. */ + public static InvokeElement newInstance( + long endpointId, long clusterId, long commandId, byte[] tlv, String jsonString) { + return new InvokeElement( + ChipPathId.forId(endpointId), + ChipPathId.forId(clusterId), + ChipPathId.forId(commandId), + Optional.of(tlv), + Optional.of(jsonString)); + } + public static InvokeElement newInstance( ChipPathId endpointId, ChipPathId clusterId, ChipPathId commandId, byte[] tlv) { - return new InvokeElement(endpointId, clusterId, commandId, tlv); + return new InvokeElement(endpointId, clusterId, commandId, Optional.of(tlv), Optional.empty()); } /** Create a new {@link InvokeElement} with only concrete ids. */ @@ -84,6 +144,39 @@ public static InvokeElement newInstance( ChipPathId.forId(endpointId), ChipPathId.forId(clusterId), ChipPathId.forId(commandId), - tlv); + Optional.of(tlv), + Optional.empty()); + } + + public static InvokeElement newInstance( + ChipPathId endpointId, ChipPathId clusterId, ChipPathId commandId, String jsonString) { + return new InvokeElement( + endpointId, clusterId, commandId, Optional.empty(), Optional.of(jsonString)); + } + + /** Create a new {@link InvokeElement} with only concrete ids. */ + public static InvokeElement newInstance( + long endpointId, long clusterId, long commandId, String jsonString) { + return new InvokeElement( + ChipPathId.forId(endpointId), + ChipPathId.forId(clusterId), + ChipPathId.forId(commandId), + Optional.empty(), + Optional.of(jsonString)); + } + + public static InvokeElement newInstance( + ChipPathId endpointId, ChipPathId clusterId, ChipPathId commandId) { + return new InvokeElement(endpointId, clusterId, commandId, Optional.empty(), Optional.empty()); + } + + /** Create a new {@link InvokeElement} with only concrete ids. */ + public static InvokeElement newInstance(long endpointId, long clusterId, long commandId) { + return new InvokeElement( + ChipPathId.forId(endpointId), + ChipPathId.forId(clusterId), + ChipPathId.forId(commandId), + Optional.empty(), + Optional.empty()); } }