| /* |
| * 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. |
| * |
| */ |
| |
| #include "OTAProviderDelegateBridge.h" |
| |
| #include <app/clusters/ota-provider/ota-provider.h> |
| #include <app/data-model/List.h> |
| #include <lib/support/JniReferences.h> |
| #include <lib/support/JniTypeWrappers.h> |
| |
| #include <string> |
| |
| using namespace chip; |
| using namespace chip::app; |
| using namespace chip::app::Clusters; |
| using namespace chip::app::Clusters::OtaSoftwareUpdateProvider; |
| using namespace chip::app::Clusters::OtaSoftwareUpdateProvider::Commands; |
| |
| constexpr EndpointId kOtaProviderEndpoint = 0; |
| |
| // Time in seconds after which the requestor should retry calling query image if |
| // busy status is receieved. The spec minimum is 2 minutes, but in practice OTA |
| // generally takes a lot longer than that and devices only retry a few times |
| // before giving up. Default to 10 minutes for now, until we have a better |
| // system of computing an expected completion time for the currently-running |
| // OTA. |
| constexpr uint32_t kDelayedActionTimeSeconds = 600; |
| |
| OTAProviderDelegateBridge::~OTAProviderDelegateBridge() |
| { |
| mBdxOTASender->ResetState(); |
| Clusters::OTAProvider::SetDelegate(kOtaProviderEndpoint, nullptr); |
| mBdxOTASender = nullptr; |
| } |
| |
| CHIP_ERROR OTAProviderDelegateBridge::Init(chip::System::Layer * systemLayer, chip::Messaging::ExchangeManager * exchangeManager, |
| jobject otaProviderDelegate) |
| { |
| ReturnLogErrorOnFailure(mOtaProviderDelegate.Init(otaProviderDelegate)); |
| |
| Clusters::OTAProvider::SetDelegate(kOtaProviderEndpoint, this); |
| mBdxOTASender = std::make_unique<BdxOTASender>(mOtaProviderDelegate.ObjectRef()); |
| return mBdxOTASender->Init(systemLayer, exchangeManager); |
| } |
| |
| void OTAProviderDelegateBridge::Shutdown() |
| { |
| VerifyOrReturn(mBdxOTASender != nullptr, ChipLogError(Controller, "BdxOTASender is null")); |
| CHIP_ERROR err = mBdxOTASender->Shutdown(); |
| if (err != CHIP_NO_ERROR) |
| { |
| ChipLogProgress(Controller, "OTAProviderDelegateBridge-Shutdown : %" CHIP_ERROR_FORMAT, err.Format()); |
| } |
| } |
| |
| void GenerateUpdateToken(uint8_t * buf, size_t bufSize) |
| { |
| for (size_t i = 0; i < bufSize; ++i) |
| { |
| buf[i] = chip::Crypto::GetRandU8(); |
| } |
| } |
| |
| void OTAProviderDelegateBridge::sendOTAQueryFailure(uint8_t status) |
| { |
| VerifyOrReturn(mOtaProviderDelegate.HasValidObjectRef(), ChipLogError(Controller, "mOtaProviderDelegate is null")); |
| |
| JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); |
| |
| jobject otaProviderDelegate = mOtaProviderDelegate.ObjectRef(); |
| jmethodID handleOTAQueryFailureMethod; |
| CHIP_ERROR err = JniReferences::GetInstance().FindMethod(env, otaProviderDelegate, "handleOTAQueryFailure", "(I)V", |
| &handleOTAQueryFailureMethod); |
| VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Could not find handleOTAQueryFailure method")); |
| |
| env->CallVoidMethod(otaProviderDelegate, handleOTAQueryFailureMethod, static_cast<jint>(status)); |
| if (env->ExceptionCheck()) |
| { |
| ChipLogError(Support, "Exception in call java method"); |
| env->ExceptionDescribe(); |
| env->ExceptionClear(); |
| return; |
| } |
| return; |
| } |
| void OTAProviderDelegateBridge::HandleQueryImage(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, |
| const QueryImage::DecodableType & commandData) |
| { |
| assertChipStackLockedByCurrentThread(); |
| |
| VerifyOrReturn(mOtaProviderDelegate.HasValidObjectRef(), |
| commandObj->AddStatus(commandPath, Protocols::InteractionModel::Status::Failure)); |
| |
| VerifyOrReturn(mBdxOTASender != nullptr, commandObj->AddStatus(commandPath, Protocols::InteractionModel::Status::Failure)); |
| |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| |
| JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); |
| VerifyOrReturn(env != nullptr, ChipLogError(Controller, "Could not get JNIEnv for current thread")); |
| chip::JniLocalReferenceScope scope(env); |
| |
| jobject otaProviderDelegate = nullptr; |
| jmethodID handleQueryImageMethod = nullptr; |
| |
| jobject boxedHardwareVersion = nullptr; |
| jobject boxedLocation = nullptr; |
| jobject boxedRequestorCanConsent = nullptr; |
| jobject boxedMetadataForProvider = nullptr; |
| jobject jResponse = nullptr; |
| jobject boxedDelayedActionTime = nullptr; |
| jobject boxedUserConsentNeeded = nullptr; |
| |
| jmethodID getSoftwareVersionMethod = nullptr; |
| jmethodID getSoftwareVersionStringMethod = nullptr; |
| jmethodID getFilePathMethod = nullptr; |
| jmethodID getStatushMethod = nullptr; |
| jmethodID getDelayedActionTimeMethod = nullptr; |
| jmethodID getUserConsentNeededMethod = nullptr; |
| |
| jobject jSoftwareVersion = nullptr; |
| jstring jSoftwareVersionString = nullptr; |
| jstring jFilePath = nullptr; |
| |
| jint jStatus = 0; |
| uint8_t status = 0; |
| |
| bool hasUpdate = false; |
| Commands::QueryImageResponse::Type response; |
| response.status = OTAQueryStatus::kNotAvailable; |
| |
| char uriBuffer[kMaxBDXURILen]; |
| MutableCharSpan uri(uriBuffer); |
| |
| NodeId nodeId = commandObj->GetSubjectDescriptor().subject; |
| |
| FabricIndex fabricIndex = commandObj->GetAccessingFabricIndex(); |
| ScopedNodeId ourNodeId = commandObj->GetExchangeContext()->GetSessionHandle()->AsSecureSession()->GetLocalScopedNodeId(); |
| VendorId vendorId = commandData.vendorID; |
| uint16_t productId = commandData.productID; |
| uint32_t softwareVersion = commandData.softwareVersion; |
| DataModel::DecodableList<OTADownloadProtocol> protocolsSupported = commandData.protocolsSupported; |
| Optional<uint16_t> hardwareVersion = commandData.hardwareVersion; |
| Optional<chip::CharSpan> location = commandData.location; |
| Optional<bool> requestorCanConsent = commandData.requestorCanConsent; |
| Optional<chip::ByteSpan> metadataForProvider = commandData.metadataForProvider; |
| |
| bool isBDXProtocolSupported = false; |
| |
| auto iterator = commandData.protocolsSupported.begin(); |
| while (iterator.Next()) |
| { |
| OTADownloadProtocol protocol = iterator.GetValue(); |
| if (protocol == OTADownloadProtocol::kBDXSynchronous) |
| { |
| isBDXProtocolSupported = true; |
| break; |
| } |
| } |
| |
| ConcreteCommandPath cachedCommandPath(commandPath.mEndpointId, commandPath.mClusterId, commandPath.mCommandId); |
| |
| // The logic we are following here is if none of the protocols supported by the requestor are supported by us, we |
| // can't transfer the image even if we had an image available and we would return a Protocol Not Supported status. |
| // Assumption here is the requestor would send us a list of all the protocols it supports. If one/more of the |
| // protocols supported by the requestor are supported by us, we check if an image is not available due to various |
| // reasons - image not available, delegate reporting busy, we will respond with the status in the delegate response. |
| // If update is available, we try to prepare for transfer and build the uri in the response with a status of Image |
| // Available |
| |
| // If the protocol requested is not supported, return status - Protocol Not Supported |
| if (!isBDXProtocolSupported) |
| { |
| response.status = OTAQueryStatus::kDownloadProtocolNotSupported; |
| err = CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; |
| ExitNow(); |
| } |
| |
| otaProviderDelegate = mOtaProviderDelegate.ObjectRef(); |
| err = JniReferences::GetInstance().FindMethod(env, otaProviderDelegate, "handleQueryImage", |
| "(IIJLjava/lang/Integer;Ljava/lang/String;Ljava/lang/Boolean;[B)Lchip/" |
| "devicecontroller/OTAProviderDelegate$QueryImageResponse;", |
| &handleQueryImageMethod); |
| VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Could not find handleQueryImage method")); |
| |
| if (hardwareVersion.HasValue()) |
| { |
| chip::JniReferences::GetInstance().CreateBoxedObject<jint>( |
| "java/lang/Integer", "(I)V", static_cast<jint>(hardwareVersion.Value()), boxedHardwareVersion); |
| } |
| |
| if (location.HasValue()) |
| { |
| boxedLocation = env->NewStringUTF(std::string(location.Value().data(), location.Value().size()).c_str()); |
| } |
| |
| if (requestorCanConsent.HasValue()) |
| { |
| chip::JniReferences::GetInstance().CreateBoxedObject<jboolean>( |
| "java/lang/Boolean", "(Z)V", requestorCanConsent.Value() ? JNI_TRUE : JNI_FALSE, boxedRequestorCanConsent); |
| } |
| |
| if (metadataForProvider.HasValue()) |
| { |
| jbyteArray boxedMetadataForProviderByteArray = env->NewByteArray(static_cast<jsize>(metadataForProvider.Value().size())); |
| env->SetByteArrayRegion(boxedMetadataForProviderByteArray, 0, static_cast<jsize>(metadataForProvider.Value().size()), |
| reinterpret_cast<const jbyte *>(metadataForProvider.Value().data())); |
| boxedMetadataForProvider = boxedMetadataForProviderByteArray; |
| } |
| |
| jResponse = env->CallObjectMethod(otaProviderDelegate, handleQueryImageMethod, static_cast<jint>(vendorId), |
| static_cast<jint>(productId), static_cast<jlong>(softwareVersion), boxedHardwareVersion, |
| boxedLocation, boxedRequestorCanConsent, boxedMetadataForProvider); |
| if (env->ExceptionCheck()) |
| { |
| ChipLogError(Support, "Exception in call java method"); |
| env->ExceptionDescribe(); |
| env->ExceptionClear(); |
| err = CHIP_JNI_ERROR_EXCEPTION_THROWN; |
| ExitNow(); |
| } |
| |
| hasUpdate = (jResponse != nullptr); |
| // If update is not available, return the delegate response |
| if (!hasUpdate) |
| { |
| response.status = OTAQueryStatus::kNotAvailable; |
| err = CHIP_ERROR_INTERNAL; |
| ExitNow(); |
| } |
| |
| err = JniReferences::GetInstance().FindMethod(env, jResponse, "getSoftwareVersion", "()Ljava/lang/Long;", |
| &getSoftwareVersionMethod); |
| VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Could not find getSoftwareVersion method")); |
| |
| err = JniReferences::GetInstance().FindMethod(env, jResponse, "getSoftwareVersionString", "()Ljava/lang/String;", |
| &getSoftwareVersionStringMethod); |
| VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Could not find getSoftwareVersionString method")); |
| |
| err = JniReferences::GetInstance().FindMethod(env, jResponse, "getFilePath", "()Ljava/lang/String;", &getFilePathMethod); |
| VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Could not find getFilePath method")); |
| |
| err = JniReferences::GetInstance().FindMethod(env, jResponse, "getStatus", "()I", &getStatushMethod); |
| VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Could not find getStatus method")); |
| |
| err = JniReferences::GetInstance().FindMethod(env, jResponse, "getDelayedActionTime", "()Ljava/lang/Long;", |
| &getDelayedActionTimeMethod); |
| VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Could not find getDelayedActionTime method")); |
| |
| err = JniReferences::GetInstance().FindMethod(env, jResponse, "getUserConsentNeeded", "()Ljava/lang/Boolean;", |
| &getUserConsentNeededMethod); |
| VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Could not find getUserConsentNeeded method")); |
| |
| jStatus = env->CallIntMethod(jResponse, getStatushMethod); |
| boxedUserConsentNeeded = env->CallObjectMethod(jResponse, getUserConsentNeededMethod); |
| |
| // UserConsentNeeded Field, if present, SHALL only be interpreted if the OTA Requestor had previously indicated a value of True |
| // in the RequestorCanConsent field of the QueryImageRequest. |
| if (boxedUserConsentNeeded != nullptr) |
| { |
| jboolean userConsentNeeded = JniReferences::GetInstance().BooleanToPrimitive(boxedUserConsentNeeded); |
| response.userConsentNeeded.SetValue(userConsentNeeded == JNI_TRUE); |
| } |
| |
| status = static_cast<uint8_t>(jStatus); |
| if (status == static_cast<uint8_t>(OTAQueryStatus::kNotAvailable)) |
| { |
| err = CHIP_ERROR_INTERNAL; |
| response.status = OTAQueryStatus::kNotAvailable; |
| ExitNow(); |
| } |
| else if (status == static_cast<uint8_t>(OTAQueryStatus::kBusy)) |
| { |
| err = CHIP_ERROR_BUSY; |
| response.status = OTAQueryStatus::kBusy; |
| boxedDelayedActionTime = env->CallObjectMethod(jResponse, getDelayedActionTimeMethod); |
| response.delayedActionTime.SetValue( |
| static_cast<uint32_t>(JniReferences::GetInstance().LongToPrimitive(boxedDelayedActionTime))); |
| ExitNow(); |
| } |
| |
| jSoftwareVersion = env->CallObjectMethod(jResponse, getSoftwareVersionMethod); |
| jSoftwareVersionString = (jstring) env->CallObjectMethod(jResponse, getSoftwareVersionStringMethod); |
| jFilePath = (jstring) env->CallObjectMethod(jResponse, getFilePathMethod); |
| |
| { |
| response.status = OTAQueryStatus::kUpdateAvailable; |
| if (jSoftwareVersion != nullptr) |
| { |
| response.softwareVersion.SetValue( |
| static_cast<uint32_t>(JniReferences::GetInstance().LongToPrimitive(jSoftwareVersion))); |
| } |
| |
| JniUtfString jniSoftwareVersionString(env, jSoftwareVersionString); |
| |
| if (jniSoftwareVersionString.c_str() != nullptr) |
| { |
| response.softwareVersionString.SetValue(jniSoftwareVersionString.charSpan()); |
| } |
| |
| JniUtfString jniFilePath(env, jFilePath); |
| |
| GenerateUpdateToken(mToken, kUpdateTokenLen); |
| |
| response.updateToken.SetValue(chip::ByteSpan(mToken, kUpdateTokenLen)); |
| response.userConsentNeeded.SetValue(0); |
| |
| err = mBdxOTASender->PrepareForTransfer(fabricIndex, nodeId); |
| if (CHIP_NO_ERROR != err) |
| { |
| // Handle busy error separately as we have a query image response status that maps to busy |
| if (err == CHIP_ERROR_BUSY) |
| { |
| ChipLogError(Controller, "Responding with Busy due to being in the middle of handling another BDX transfer"); |
| response.status = OTAQueryStatus::kBusy; |
| response.delayedActionTime.SetValue(kDelayedActionTimeSeconds); |
| // We do not reset state when we get the busy error because that means we are locked in a BDX transfer |
| // session with another requestor when we get this query image request. We do not want to interrupt the |
| // ongoing transfer instead just respond to the second requestor with a busy status and a delayedActionTime |
| // in which the requestor can retry. |
| ExitNow(); |
| } |
| LogErrorOnFailure(err); |
| commandObj->AddStatus(cachedCommandPath, StatusIB(err).mStatus); |
| // We need to reset state here to clean up any initialization we might have done including starting the BDX |
| // timeout timer while preparing for transfer if any failure occurs afterwards. |
| mBdxOTASender->ResetState(); |
| return; |
| } |
| err = bdx::MakeURI(ourNodeId.GetNodeId(), jniFilePath.c_str() != nullptr ? jniFilePath.charSpan() : CharSpan(), uri); |
| if (CHIP_NO_ERROR != err) |
| { |
| LogErrorOnFailure(err); |
| commandObj->AddStatus(cachedCommandPath, StatusIB(err).mStatus); |
| mBdxOTASender->ResetState(); |
| return; |
| } |
| response.imageURI.SetValue(uri); |
| commandObj->AddResponse(cachedCommandPath, response); |
| } |
| return; |
| |
| exit: |
| ChipLogError(Controller, "OTA Query Failure : %u, %" CHIP_ERROR_FORMAT, static_cast<uint8_t>(response.status), err.Format()); |
| commandObj->AddResponse(cachedCommandPath, response); |
| sendOTAQueryFailure(static_cast<uint8_t>(response.status)); |
| } |
| |
| void OTAProviderDelegateBridge::HandleApplyUpdateRequest(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, |
| const ApplyUpdateRequest::DecodableType & commandData) |
| { |
| assertChipStackLockedByCurrentThread(); |
| |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| NodeId nodeId = kUndefinedNodeId; |
| ConcreteCommandPath cachedCommandPath(commandPath.mEndpointId, commandPath.mClusterId, commandPath.mCommandId); |
| |
| JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); |
| |
| jobject otaProviderDelegate = nullptr; |
| jobject jResponse = nullptr; |
| jmethodID handleApplyUpdateRequestMethod; |
| jmethodID getActionMethod; |
| jmethodID getDelayedActionTimeMethod; |
| |
| chip::JniLocalReferenceScope scope(env); |
| |
| Commands::ApplyUpdateResponse::Type response; |
| Commands::ApplyUpdateResponse::Type errorResponse; |
| errorResponse.action = ApplyUpdateActionEnum::kDiscontinue; |
| errorResponse.delayedActionTime = 0; |
| |
| jint jAction = 0; |
| jlong jDelayedActionTime = 0; |
| |
| VerifyOrReturn(mOtaProviderDelegate.HasValidObjectRef(), |
| commandObj->AddStatus(commandPath, Protocols::InteractionModel::Status::Failure)); |
| |
| nodeId = commandObj->GetSubjectDescriptor().subject; |
| |
| otaProviderDelegate = mOtaProviderDelegate.ObjectRef(); |
| |
| err = JniReferences::GetInstance().FindMethod(env, otaProviderDelegate, "handleApplyUpdateRequest", |
| "(JJ)Lchip/devicecontroller/OTAProviderDelegate$ApplyUpdateResponse;", |
| &handleApplyUpdateRequestMethod); |
| VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Could not find handleApplyUpdateRequest method")); |
| |
| jResponse = env->CallObjectMethod(otaProviderDelegate, handleApplyUpdateRequestMethod, static_cast<jlong>(nodeId), |
| static_cast<jlong>(commandData.newVersion)); |
| if (env->ExceptionCheck()) |
| { |
| ChipLogError(Support, "Exception in call java method"); |
| env->ExceptionDescribe(); |
| env->ExceptionClear(); |
| err = CHIP_JNI_ERROR_EXCEPTION_THROWN; |
| ExitNow(); |
| } |
| |
| err = JniReferences::GetInstance().FindMethod(env, jResponse, "getAction", "()I", &getActionMethod); |
| VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Could not find getAction method")); |
| |
| err = JniReferences::GetInstance().FindMethod(env, jResponse, "getDelayedActionTime", "()J", &getDelayedActionTimeMethod); |
| VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Could not find getDelayedActionTime method")); |
| |
| jAction = env->CallIntMethod(jResponse, getActionMethod); |
| jDelayedActionTime = env->CallLongMethod(jResponse, getDelayedActionTimeMethod); |
| |
| response.action = static_cast<OTAApplyUpdateAction>(jAction); |
| response.delayedActionTime = static_cast<uint32_t>(jDelayedActionTime); |
| commandObj->AddResponse(cachedCommandPath, response); |
| return; |
| |
| exit: |
| ChipLogError(Controller, "Apply Update Request Failure : %" CHIP_ERROR_FORMAT, err.Format()); |
| commandObj->AddResponse(cachedCommandPath, errorResponse); |
| } |
| |
| void OTAProviderDelegateBridge::HandleNotifyUpdateApplied(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, |
| const NotifyUpdateApplied::DecodableType & commandData) |
| { |
| assertChipStackLockedByCurrentThread(); |
| |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| |
| VerifyOrReturn(mOtaProviderDelegate.HasValidObjectRef(), |
| commandObj->AddStatus(commandPath, Protocols::InteractionModel::Status::Failure)); |
| |
| NodeId nodeId = commandObj->GetSubjectDescriptor().subject; |
| |
| ConcreteCommandPath cachedCommandPath(commandPath.mEndpointId, commandPath.mClusterId, commandPath.mCommandId); |
| |
| JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); |
| |
| jobject otaProviderDelegate = mOtaProviderDelegate.ObjectRef(); |
| jmethodID handleNotifyUpdateAppliedMethod; |
| err = JniReferences::GetInstance().FindMethod(env, otaProviderDelegate, "handleNotifyUpdateApplied", "(J)V", |
| &handleNotifyUpdateAppliedMethod); |
| VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Could not find handleNotifyUpdateApplied method")); |
| |
| env->CallVoidMethod(otaProviderDelegate, handleNotifyUpdateAppliedMethod, static_cast<jlong>(nodeId)); |
| if (env->ExceptionCheck()) |
| { |
| ChipLogError(Support, "Exception in call java method"); |
| env->ExceptionDescribe(); |
| env->ExceptionClear(); |
| err = CHIP_JNI_ERROR_EXCEPTION_THROWN; |
| ExitNow(); |
| } |
| |
| commandObj->AddStatus(cachedCommandPath, Protocols::InteractionModel::Status::Success); |
| return; |
| |
| exit: |
| ChipLogError(Controller, "Notify Update Applied Failure : %" CHIP_ERROR_FORMAT, err.Format()); |
| commandObj->AddStatus(commandPath, Protocols::InteractionModel::Status::Failure); |
| } |