| /* |
| * 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. |
| */ |
| |
| /** |
| * @file |
| * This file defines a basic implementation of SubscriptionResumptionStorage that |
| * persists subscriptions in a flat list in TLV. |
| */ |
| |
| #include <app/SimpleSubscriptionResumptionStorage.h> |
| |
| #include <lib/support/Base64.h> |
| #include <lib/support/CodeUtils.h> |
| #include <lib/support/SafeInt.h> |
| #include <lib/support/logging/CHIPLogging.h> |
| |
| namespace chip { |
| namespace app { |
| |
| constexpr TLV::Tag SimpleSubscriptionResumptionStorage::kPeerNodeIdTag; |
| constexpr TLV::Tag SimpleSubscriptionResumptionStorage::kFabricIndexTag; |
| constexpr TLV::Tag SimpleSubscriptionResumptionStorage::kSubscriptionIdTag; |
| constexpr TLV::Tag SimpleSubscriptionResumptionStorage::kMinIntervalTag; |
| constexpr TLV::Tag SimpleSubscriptionResumptionStorage::kMaxIntervalTag; |
| constexpr TLV::Tag SimpleSubscriptionResumptionStorage::kFabricFilteredTag; |
| constexpr TLV::Tag SimpleSubscriptionResumptionStorage::kAttributePathsListTag; |
| constexpr TLV::Tag SimpleSubscriptionResumptionStorage::kEventPathsListTag; |
| constexpr TLV::Tag SimpleSubscriptionResumptionStorage::kAttributePathTag; |
| constexpr TLV::Tag SimpleSubscriptionResumptionStorage::kEventPathTag; |
| constexpr TLV::Tag SimpleSubscriptionResumptionStorage::kEndpointIdTag; |
| constexpr TLV::Tag SimpleSubscriptionResumptionStorage::kClusterIdTag; |
| constexpr TLV::Tag SimpleSubscriptionResumptionStorage::kAttributeIdTag; |
| constexpr TLV::Tag SimpleSubscriptionResumptionStorage::kEventIdTag; |
| constexpr TLV::Tag SimpleSubscriptionResumptionStorage::kEventPathTypeTag; |
| constexpr TLV::Tag SimpleSubscriptionResumptionStorage::kResumptionRetriesTag; |
| |
| SimpleSubscriptionResumptionStorage::SimpleSubscriptionInfoIterator::SimpleSubscriptionInfoIterator( |
| SimpleSubscriptionResumptionStorage & storage) : |
| mStorage(storage) |
| { |
| mNextIndex = 0; |
| } |
| |
| size_t SimpleSubscriptionResumptionStorage::SimpleSubscriptionInfoIterator::Count() |
| { |
| return static_cast<size_t>(mStorage.Count()); |
| } |
| |
| bool SimpleSubscriptionResumptionStorage::SimpleSubscriptionInfoIterator::Next(SubscriptionInfo & output) |
| { |
| for (; mNextIndex < CHIP_IM_MAX_NUM_SUBSCRIPTIONS; mNextIndex++) |
| { |
| CHIP_ERROR err = mStorage.Load(mNextIndex, output); |
| if (err == CHIP_NO_ERROR) |
| { |
| // increment index for the next call |
| mNextIndex++; |
| return true; |
| } |
| |
| if (err != CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND) |
| { |
| ChipLogError(DataManagement, "Failed to load subscription at index %u error %" CHIP_ERROR_FORMAT, |
| static_cast<unsigned>(mNextIndex), err.Format()); |
| mStorage.Delete(mNextIndex); |
| } |
| } |
| |
| return false; |
| } |
| |
| void SimpleSubscriptionResumptionStorage::SimpleSubscriptionInfoIterator::Release() |
| { |
| mStorage.mSubscriptionInfoIterators.ReleaseObject(this); |
| } |
| |
| CHIP_ERROR SimpleSubscriptionResumptionStorage::Init(PersistentStorageDelegate * storage) |
| { |
| VerifyOrReturnError(storage != nullptr, CHIP_ERROR_INVALID_ARGUMENT); |
| mStorage = storage; |
| |
| uint16_t countMax; |
| uint16_t len = sizeof(countMax); |
| CHIP_ERROR err = |
| mStorage->SyncGetKeyValue(DefaultStorageKeyAllocator::SubscriptionResumptionMaxCount().KeyName(), &countMax, len); |
| // If there's a previous countMax and it's larger than CHIP_IM_MAX_NUM_SUBSCRIPTIONS, |
| // clean up subscriptions beyond the limit |
| if ((err == CHIP_NO_ERROR) && (countMax != CHIP_IM_MAX_NUM_SUBSCRIPTIONS)) |
| { |
| for (uint16_t subscriptionIndex = CHIP_IM_MAX_NUM_SUBSCRIPTIONS; subscriptionIndex < countMax; subscriptionIndex++) |
| { |
| Delete(subscriptionIndex); |
| } |
| } |
| |
| // Always save the current CHIP_IM_MAX_NUM_SUBSCRIPTIONS |
| uint16_t countMaxToSave = CHIP_IM_MAX_NUM_SUBSCRIPTIONS; |
| ReturnErrorOnFailure(mStorage->SyncSetKeyValue(DefaultStorageKeyAllocator::SubscriptionResumptionMaxCount().KeyName(), |
| &countMaxToSave, sizeof(uint16_t))); |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| SubscriptionResumptionStorage::SubscriptionInfoIterator * SimpleSubscriptionResumptionStorage::IterateSubscriptions() |
| { |
| return mSubscriptionInfoIterators.CreateObject(*this); |
| } |
| |
| uint16_t SimpleSubscriptionResumptionStorage::Count() |
| { |
| uint16_t subscriptionCount = 0; |
| for (uint16_t subscriptionIndex = 0; subscriptionIndex < CHIP_IM_MAX_NUM_SUBSCRIPTIONS; subscriptionIndex++) |
| { |
| if (mStorage->SyncDoesKeyExist(DefaultStorageKeyAllocator::SubscriptionResumption(subscriptionIndex).KeyName())) |
| { |
| subscriptionCount++; |
| } |
| } |
| |
| return subscriptionCount; |
| } |
| |
| CHIP_ERROR SimpleSubscriptionResumptionStorage::Delete(uint16_t subscriptionIndex) |
| { |
| return mStorage->SyncDeleteKeyValue(DefaultStorageKeyAllocator::SubscriptionResumption(subscriptionIndex).KeyName()); |
| } |
| |
| CHIP_ERROR SimpleSubscriptionResumptionStorage::Load(uint16_t subscriptionIndex, SubscriptionInfo & subscriptionInfo) |
| { |
| Platform::ScopedMemoryBuffer<uint8_t> backingBuffer; |
| backingBuffer.Calloc(MaxSubscriptionSize()); |
| VerifyOrReturnError(backingBuffer.Get() != nullptr, CHIP_ERROR_NO_MEMORY); |
| |
| uint16_t len = static_cast<uint16_t>(MaxSubscriptionSize()); |
| ReturnErrorOnFailure(mStorage->SyncGetKeyValue(DefaultStorageKeyAllocator::SubscriptionResumption(subscriptionIndex).KeyName(), |
| backingBuffer.Get(), len)); |
| |
| TLV::ScopedBufferTLVReader reader(std::move(backingBuffer), len); |
| |
| ReturnErrorOnFailure(reader.Next(TLV::kTLVType_Structure, TLV::AnonymousTag())); |
| |
| TLV::TLVType subscriptionContainerType; |
| ReturnErrorOnFailure(reader.EnterContainer(subscriptionContainerType)); |
| |
| // Node ID |
| ReturnErrorOnFailure(reader.Next(kPeerNodeIdTag)); |
| ReturnErrorOnFailure(reader.Get(subscriptionInfo.mNodeId)); |
| |
| // Fabric index |
| ReturnErrorOnFailure(reader.Next(kFabricIndexTag)); |
| ReturnErrorOnFailure(reader.Get(subscriptionInfo.mFabricIndex)); |
| |
| // Subscription ID |
| ReturnErrorOnFailure(reader.Next(kSubscriptionIdTag)); |
| ReturnErrorOnFailure(reader.Get(subscriptionInfo.mSubscriptionId)); |
| |
| // Min interval |
| ReturnErrorOnFailure(reader.Next(kMinIntervalTag)); |
| ReturnErrorOnFailure(reader.Get(subscriptionInfo.mMinInterval)); |
| |
| // Max interval |
| ReturnErrorOnFailure(reader.Next(kMaxIntervalTag)); |
| ReturnErrorOnFailure(reader.Get(subscriptionInfo.mMaxInterval)); |
| |
| // Fabric filtered boolean |
| ReturnErrorOnFailure(reader.Next(kFabricFilteredTag)); |
| ReturnErrorOnFailure(reader.Get(subscriptionInfo.mFabricFiltered)); |
| |
| // Attribute Paths |
| ReturnErrorOnFailure(reader.Next(TLV::kTLVType_List, kAttributePathsListTag)); |
| TLV::TLVType attributesListType; |
| ReturnErrorOnFailure(reader.EnterContainer(attributesListType)); |
| |
| size_t pathCount = 0; |
| ReturnErrorOnFailure(reader.CountRemainingInContainer(&pathCount)); |
| |
| // If a stack struct is being reused to iterate, free the previous paths ScopedMemoryBuffer |
| subscriptionInfo.mAttributePaths.Free(); |
| if (pathCount) |
| { |
| subscriptionInfo.mAttributePaths.Calloc(pathCount); |
| VerifyOrReturnError(subscriptionInfo.mAttributePaths.Get() != nullptr, CHIP_ERROR_NO_MEMORY); |
| for (size_t pathIndex = 0; pathIndex < pathCount; pathIndex++) |
| { |
| ReturnErrorOnFailure(reader.Next(TLV::kTLVType_Structure, kAttributePathTag)); |
| TLV::TLVType attributeContainerType; |
| ReturnErrorOnFailure(reader.EnterContainer(attributeContainerType)); |
| |
| ReturnErrorOnFailure(reader.Next(kEndpointIdTag)); |
| ReturnErrorOnFailure(reader.Get(subscriptionInfo.mAttributePaths[pathIndex].mEndpointId)); |
| |
| ReturnErrorOnFailure(reader.Next(kClusterIdTag)); |
| ReturnErrorOnFailure(reader.Get(subscriptionInfo.mAttributePaths[pathIndex].mClusterId)); |
| |
| ReturnErrorOnFailure(reader.Next(kAttributeIdTag)); |
| ReturnErrorOnFailure(reader.Get(subscriptionInfo.mAttributePaths[pathIndex].mAttributeId)); |
| |
| ReturnErrorOnFailure(reader.ExitContainer(attributeContainerType)); |
| } |
| } |
| ReturnErrorOnFailure(reader.ExitContainer(attributesListType)); |
| |
| // Event Paths |
| ReturnErrorOnFailure(reader.Next(TLV::kTLVType_List, kEventPathsListTag)); |
| TLV::TLVType eventsListType; |
| ReturnErrorOnFailure(reader.EnterContainer(eventsListType)); |
| |
| ReturnErrorOnFailure(reader.CountRemainingInContainer(&pathCount)); |
| |
| // If a stack struct is being reused to iterate, free the previous paths ScopedMemoryBuffer |
| subscriptionInfo.mEventPaths.Free(); |
| if (pathCount) |
| { |
| subscriptionInfo.mEventPaths.Calloc(pathCount); |
| VerifyOrReturnError(subscriptionInfo.mEventPaths.Get() != nullptr, CHIP_ERROR_NO_MEMORY); |
| for (size_t pathIndex = 0; pathIndex < pathCount; pathIndex++) |
| { |
| ReturnErrorOnFailure(reader.Next(TLV::kTLVType_Structure, kEventPathTag)); |
| TLV::TLVType eventContainerType; |
| ReturnErrorOnFailure(reader.EnterContainer(eventContainerType)); |
| |
| EventPathType eventPathType; |
| ReturnErrorOnFailure(reader.Next(kEventPathTypeTag)); |
| ReturnErrorOnFailure(reader.Get(eventPathType)); |
| |
| subscriptionInfo.mEventPaths[pathIndex].mIsUrgentEvent = (eventPathType == EventPathType::kUrgent); |
| |
| ReturnErrorOnFailure(reader.Next(kEndpointIdTag)); |
| ReturnErrorOnFailure(reader.Get(subscriptionInfo.mEventPaths[pathIndex].mEndpointId)); |
| |
| ReturnErrorOnFailure(reader.Next(kClusterIdTag)); |
| ReturnErrorOnFailure(reader.Get(subscriptionInfo.mEventPaths[pathIndex].mClusterId)); |
| |
| ReturnErrorOnFailure(reader.Next(kEventIdTag)); |
| ReturnErrorOnFailure(reader.Get(subscriptionInfo.mEventPaths[pathIndex].mEventId)); |
| |
| ReturnErrorOnFailure(reader.ExitContainer(eventContainerType)); |
| } |
| } |
| ReturnErrorOnFailure(reader.ExitContainer(eventsListType)); |
| |
| #if CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION |
| // If the reader cannot get resumption retries, set it to 0 for subscriptionInfo |
| if (reader.Next(kResumptionRetriesTag) == CHIP_NO_ERROR) |
| { |
| ReturnErrorOnFailure(reader.Get(subscriptionInfo.mResumptionRetries)); |
| } |
| else |
| { |
| subscriptionInfo.mResumptionRetries = 0; |
| } |
| #endif // CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION |
| |
| ReturnErrorOnFailure(reader.ExitContainer(subscriptionContainerType)); |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| CHIP_ERROR SimpleSubscriptionResumptionStorage::Save(TLV::TLVWriter & writer, SubscriptionInfo & subscriptionInfo) |
| { |
| TLV::TLVType subscriptionContainerType; |
| ReturnErrorOnFailure(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, subscriptionContainerType)); |
| ReturnErrorOnFailure(writer.Put(kPeerNodeIdTag, subscriptionInfo.mNodeId)); |
| ReturnErrorOnFailure(writer.Put(kFabricIndexTag, subscriptionInfo.mFabricIndex)); |
| ReturnErrorOnFailure(writer.Put(kSubscriptionIdTag, subscriptionInfo.mSubscriptionId)); |
| ReturnErrorOnFailure(writer.Put(kMinIntervalTag, subscriptionInfo.mMinInterval)); |
| ReturnErrorOnFailure(writer.Put(kMaxIntervalTag, subscriptionInfo.mMaxInterval)); |
| ReturnErrorOnFailure(writer.Put(kFabricFilteredTag, subscriptionInfo.mFabricFiltered)); |
| |
| // Attribute paths |
| TLV::TLVType attributesListType; |
| ReturnErrorOnFailure(writer.StartContainer(kAttributePathsListTag, TLV::kTLVType_List, attributesListType)); |
| for (size_t pathIndex = 0; pathIndex < subscriptionInfo.mAttributePaths.AllocatedSize(); pathIndex++) |
| { |
| TLV::TLVType attributeContainerType = TLV::kTLVType_Structure; |
| ReturnErrorOnFailure(writer.StartContainer(kAttributePathTag, TLV::kTLVType_Structure, attributeContainerType)); |
| |
| ReturnErrorOnFailure(writer.Put(kEndpointIdTag, subscriptionInfo.mAttributePaths[pathIndex].mEndpointId)); |
| ReturnErrorOnFailure(writer.Put(kClusterIdTag, subscriptionInfo.mAttributePaths[pathIndex].mClusterId)); |
| ReturnErrorOnFailure(writer.Put(kAttributeIdTag, subscriptionInfo.mAttributePaths[pathIndex].mAttributeId)); |
| |
| ReturnErrorOnFailure(writer.EndContainer(attributeContainerType)); |
| } |
| ReturnErrorOnFailure(writer.EndContainer(attributesListType)); |
| |
| // Event paths |
| TLV::TLVType eventsListType; |
| ReturnErrorOnFailure(writer.StartContainer(kEventPathsListTag, TLV::kTLVType_List, eventsListType)); |
| for (size_t pathIndex = 0; pathIndex < subscriptionInfo.mEventPaths.AllocatedSize(); pathIndex++) |
| { |
| TLV::TLVType eventContainerType = TLV::kTLVType_Structure; |
| ReturnErrorOnFailure(writer.StartContainer(kEventPathTag, TLV::kTLVType_Structure, eventContainerType)); |
| |
| if (subscriptionInfo.mEventPaths[pathIndex].mIsUrgentEvent) |
| { |
| ReturnErrorOnFailure(writer.Put(kEventPathTypeTag, EventPathType::kUrgent)); |
| } |
| else |
| { |
| ReturnErrorOnFailure(writer.Put(kEventPathTypeTag, EventPathType::kNonUrgent)); |
| } |
| ReturnErrorOnFailure(writer.Put(kEndpointIdTag, subscriptionInfo.mEventPaths[pathIndex].mEndpointId)); |
| ReturnErrorOnFailure(writer.Put(kClusterIdTag, subscriptionInfo.mEventPaths[pathIndex].mClusterId)); |
| ReturnErrorOnFailure(writer.Put(kEventIdTag, subscriptionInfo.mEventPaths[pathIndex].mEventId)); |
| |
| ReturnErrorOnFailure(writer.EndContainer(eventContainerType)); |
| } |
| ReturnErrorOnFailure(writer.EndContainer(eventsListType)); |
| #if CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION |
| ReturnErrorOnFailure(writer.Put(kResumptionRetriesTag, subscriptionInfo.mResumptionRetries)); |
| #endif // CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION |
| |
| ReturnErrorOnFailure(writer.EndContainer(subscriptionContainerType)); |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| CHIP_ERROR SimpleSubscriptionResumptionStorage::Save(SubscriptionInfo & subscriptionInfo) |
| { |
| // Find empty index or duplicate if exists |
| uint16_t subscriptionIndex; |
| uint16_t firstEmptySubscriptionIndex = CHIP_IM_MAX_NUM_SUBSCRIPTIONS; // initialize to out of bounds as "not set" |
| for (subscriptionIndex = 0; subscriptionIndex < CHIP_IM_MAX_NUM_SUBSCRIPTIONS; subscriptionIndex++) |
| { |
| SubscriptionInfo currentSubscriptionInfo; |
| CHIP_ERROR err = Load(subscriptionIndex, currentSubscriptionInfo); |
| |
| // if empty and firstEmptySubscriptionIndex isn't set yet, then mark empty spot |
| if ((firstEmptySubscriptionIndex == CHIP_IM_MAX_NUM_SUBSCRIPTIONS) && (err == CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND)) |
| { |
| firstEmptySubscriptionIndex = subscriptionIndex; |
| } |
| |
| // delete duplicate |
| if (err == CHIP_NO_ERROR) |
| { |
| if ((subscriptionInfo.mNodeId == currentSubscriptionInfo.mNodeId) && |
| (subscriptionInfo.mFabricIndex == currentSubscriptionInfo.mFabricIndex) && |
| (subscriptionInfo.mSubscriptionId == currentSubscriptionInfo.mSubscriptionId)) |
| { |
| Delete(subscriptionIndex); |
| // if duplicate is the first empty spot, then also set it |
| if (firstEmptySubscriptionIndex == CHIP_IM_MAX_NUM_SUBSCRIPTIONS) |
| { |
| firstEmptySubscriptionIndex = subscriptionIndex; |
| } |
| } |
| } |
| } |
| |
| // Fail if no empty space |
| if (firstEmptySubscriptionIndex == CHIP_IM_MAX_NUM_SUBSCRIPTIONS) |
| { |
| return CHIP_ERROR_NO_MEMORY; |
| } |
| |
| // Now construct subscription state and save |
| Platform::ScopedMemoryBuffer<uint8_t> backingBuffer; |
| backingBuffer.Calloc(MaxSubscriptionSize()); |
| VerifyOrReturnError(backingBuffer.Get() != nullptr, CHIP_ERROR_NO_MEMORY); |
| |
| TLV::ScopedBufferTLVWriter writer(std::move(backingBuffer), MaxSubscriptionSize()); |
| |
| ReturnErrorOnFailure(Save(writer, subscriptionInfo)); |
| |
| const auto len = writer.GetLengthWritten(); |
| VerifyOrReturnError(CanCastTo<uint16_t>(len), CHIP_ERROR_BUFFER_TOO_SMALL); |
| |
| writer.Finalize(backingBuffer); |
| |
| ReturnErrorOnFailure( |
| mStorage->SyncSetKeyValue(DefaultStorageKeyAllocator::SubscriptionResumption(firstEmptySubscriptionIndex).KeyName(), |
| backingBuffer.Get(), static_cast<uint16_t>(len))); |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| CHIP_ERROR SimpleSubscriptionResumptionStorage::Delete(NodeId nodeId, FabricIndex fabricIndex, SubscriptionId subscriptionId) |
| { |
| bool subscriptionFound = false; |
| CHIP_ERROR lastDeleteErr = CHIP_NO_ERROR; |
| |
| uint16_t remainingSubscriptionsCount = 0; |
| for (uint16_t subscriptionIndex = 0; subscriptionIndex < CHIP_IM_MAX_NUM_SUBSCRIPTIONS; subscriptionIndex++) |
| { |
| SubscriptionInfo subscriptionInfo; |
| CHIP_ERROR err = Load(subscriptionIndex, subscriptionInfo); |
| |
| // delete match |
| if (err == CHIP_NO_ERROR) |
| { |
| if ((nodeId == subscriptionInfo.mNodeId) && (fabricIndex == subscriptionInfo.mFabricIndex) && |
| (subscriptionId == subscriptionInfo.mSubscriptionId)) |
| { |
| subscriptionFound = true; |
| CHIP_ERROR deleteErr = Delete(subscriptionIndex); |
| if (deleteErr != CHIP_NO_ERROR) |
| { |
| lastDeleteErr = deleteErr; |
| } |
| } |
| else |
| { |
| remainingSubscriptionsCount++; |
| } |
| } |
| } |
| |
| // if there are no persisted subscriptions, the MaxCount can also be deleted |
| if (remainingSubscriptionsCount == 0) |
| { |
| DeleteMaxCount(); |
| } |
| |
| if (lastDeleteErr != CHIP_NO_ERROR) |
| { |
| return lastDeleteErr; |
| } |
| |
| return subscriptionFound ? CHIP_NO_ERROR : CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND; |
| } |
| |
| CHIP_ERROR SimpleSubscriptionResumptionStorage::DeleteMaxCount() |
| { |
| return mStorage->SyncDeleteKeyValue(DefaultStorageKeyAllocator::SubscriptionResumptionMaxCount().KeyName()); |
| } |
| |
| CHIP_ERROR SimpleSubscriptionResumptionStorage::DeleteAll(FabricIndex fabricIndex) |
| { |
| CHIP_ERROR deleteErr = CHIP_NO_ERROR; |
| |
| uint16_t count = 0; |
| for (uint16_t subscriptionIndex = 0; subscriptionIndex < CHIP_IM_MAX_NUM_SUBSCRIPTIONS; subscriptionIndex++) |
| { |
| SubscriptionInfo subscriptionInfo; |
| CHIP_ERROR err = Load(subscriptionIndex, subscriptionInfo); |
| |
| if (err == CHIP_NO_ERROR) |
| { |
| if (fabricIndex == subscriptionInfo.mFabricIndex) |
| { |
| err = Delete(subscriptionIndex); |
| if ((err != CHIP_NO_ERROR) && (err != CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND)) |
| { |
| deleteErr = err; |
| } |
| } |
| else |
| { |
| count++; |
| } |
| } |
| } |
| |
| // if there are no persisted subscriptions, the MaxCount can also be deleted |
| if (count == 0) |
| { |
| CHIP_ERROR err = DeleteMaxCount(); |
| |
| if ((err != CHIP_NO_ERROR) && (err != CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND)) |
| { |
| deleteErr = err; |
| } |
| } |
| |
| return deleteErr; |
| } |
| |
| } // namespace app |
| } // namespace chip |