summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Fischer <joeljfischer@gmail.com>2021-03-04 10:42:46 -0800
committerGitHub <noreply@github.com>2021-03-04 10:42:46 -0800
commit19055a83e82f5883335aafbf646e750022055ac9 (patch)
treedc0b0ebb7031750478b633547c8ada3c612541e7
parentee70ad16db0c0eb3793196860a153bab4387cef7 (diff)
parent35e3168d91196926e8a1983f6b0ed0aa0b2d6662 (diff)
downloadsdl_ios-19055a83e82f5883335aafbf646e750022055ac9.tar.gz
Merge pull request #1886 from smartdevicelink/feature/issue-1024-sdl-0180-broaden-choiceCell-uniqueness
SDL 0180 - Fix to support broaden choice cell uniqueness
-rw-r--r--SmartDeviceLink-iOS.xcodeproj/project.pbxproj20
-rw-r--r--SmartDeviceLink/private/NSArray+Extensions.h20
-rw-r--r--SmartDeviceLink/private/NSArray+Extensions.m26
-rw-r--r--SmartDeviceLink/private/SDLChoiceSetManager.m50
-rw-r--r--SmartDeviceLink/private/SDLMacros.h16
-rw-r--r--SmartDeviceLink/private/SDLMacros.m14
-rw-r--r--SmartDeviceLink/private/SDLMenuManager.m100
-rw-r--r--SmartDeviceLink/private/SDLPreloadChoicesOperation.h2
-rw-r--r--SmartDeviceLink/private/SDLPreloadChoicesOperation.m4
-rw-r--r--SmartDeviceLink/public/SDLChoiceCell.h4
-rw-r--r--SmartDeviceLink/public/SDLChoiceCell.m7
-rw-r--r--SmartDeviceLink/public/SDLChoiceSet.h19
-rw-r--r--SmartDeviceLink/public/SDLChoiceSet.m63
-rw-r--r--SmartDeviceLink/public/SDLFile.m2
-rw-r--r--SmartDeviceLink/public/SDLMenuCell.h5
-rw-r--r--SmartDeviceLink/public/SDLMenuCell.m15
-rw-r--r--SmartDeviceLink/public/SDLScreenManager.h8
-rw-r--r--SmartDeviceLinkTests/DevAPISpecs/NSArray+ExtensionsSpec.m60
-rw-r--r--SmartDeviceLinkTests/DevAPISpecs/SDLChoiceCellSpec.m3
-rw-r--r--SmartDeviceLinkTests/DevAPISpecs/SDLChoiceSetManagerSpec.m47
-rw-r--r--SmartDeviceLinkTests/DevAPISpecs/SDLChoiceSetSpec.m24
-rw-r--r--SmartDeviceLinkTests/DevAPISpecs/SDLFileManagerSpec.m24
-rw-r--r--SmartDeviceLinkTests/DevAPISpecs/SDLMenuCellSpec.m2
-rw-r--r--SmartDeviceLinkTests/DevAPISpecs/SDLMenuManagerSpec.m79
-rw-r--r--SmartDeviceLinkTests/DevAPISpecs/SDLPreloadChoicesOperationSpec.m16
25 files changed, 523 insertions, 107 deletions
diff --git a/SmartDeviceLink-iOS.xcodeproj/project.pbxproj b/SmartDeviceLink-iOS.xcodeproj/project.pbxproj
index aa5f73691..54e0ef74e 100644
--- a/SmartDeviceLink-iOS.xcodeproj/project.pbxproj
+++ b/SmartDeviceLink-iOS.xcodeproj/project.pbxproj
@@ -301,6 +301,7 @@
4A457DD524A3C16E00386CBA /* SDLLifecycleMobileHMIStateHandlerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A457DD424A3C16E00386CBA /* SDLLifecycleMobileHMIStateHandlerSpec.m */; };
4A457DD724A3CCED00386CBA /* SDLLifecycleSystemRequestHandlerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A457DD624A3CCED00386CBA /* SDLLifecycleSystemRequestHandlerSpec.m */; };
4A457DD924A5137100386CBA /* SDLLifecycleProtocolHandlerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A457DD824A5137100386CBA /* SDLLifecycleProtocolHandlerSpec.m */; };
+ 4A5822C225E40BB5002822F1 /* NSArray+ExtensionsSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A5822C125E40BB5002822F1 /* NSArray+ExtensionsSpec.m */; };
4A8BD23B24F93135000945E3 /* SDLMassageCushionFirmness.h in Headers */ = {isa = PBXBuildFile; fileRef = 4A8BD22324F93131000945E3 /* SDLMassageCushionFirmness.h */; settings = {ATTRIBUTES = (Public, ); }; };
4A8BD23C24F93135000945E3 /* SDLMediaServiceData.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A8BD22424F93131000945E3 /* SDLMediaServiceData.m */; };
4A8BD23D24F93135000945E3 /* SDLMsgVersion.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A8BD22524F93132000945E3 /* SDLMsgVersion.m */; };
@@ -1716,6 +1717,10 @@
B3A9DA1225D270EA00CDFD21 /* SDLKeyboardLayoutCapabilitySpec.m in Sources */ = {isa = PBXBuildFile; fileRef = B3A9DA1125D270E900CDFD21 /* SDLKeyboardLayoutCapabilitySpec.m */; };
B3EC9E6E2579AA010039F3AA /* SDLClimateDataSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = B3EC9E6D2579AA010039F3AA /* SDLClimateDataSpec.m */; };
B3F7918324E062C200DB5CAF /* SDLGetVehicleDataSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 162E824C1A9BDE8A00906325 /* SDLGetVehicleDataSpec.m */; };
+ C9707D1825DEE786009D00F2 /* NSArray+Extensions.h in Headers */ = {isa = PBXBuildFile; fileRef = C9707D1625DEE786009D00F2 /* NSArray+Extensions.h */; };
+ C9707D1925DEE786009D00F2 /* NSArray+Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = C9707D1725DEE786009D00F2 /* NSArray+Extensions.m */; };
+ C9707D3025E0444D009D00F2 /* SDLMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = C9707D2E25E0444D009D00F2 /* SDLMacros.h */; };
+ C9707D3125E0444D009D00F2 /* SDLMacros.m in Sources */ = {isa = PBXBuildFile; fileRef = C9707D2F25E0444D009D00F2 /* SDLMacros.m */; };
C975877F257AEFDB0066F271 /* SDLSeekIndicatorTypeSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = C975877E257AEFDB0066F271 /* SDLSeekIndicatorTypeSpec.m */; };
C9758785257F4C570066F271 /* SDLSeekStreamingIndicatorSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = C9758784257F4C570066F271 /* SDLSeekStreamingIndicatorSpec.m */; };
C9DFFE78257ACE0000F7D57A /* SDLSeekStreamingIndicator.h in Headers */ = {isa = PBXBuildFile; fileRef = C9DFFE76257ACE0000F7D57A /* SDLSeekStreamingIndicator.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -2132,6 +2137,7 @@
4A457DD424A3C16E00386CBA /* SDLLifecycleMobileHMIStateHandlerSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLLifecycleMobileHMIStateHandlerSpec.m; path = DevAPISpecs/SDLLifecycleMobileHMIStateHandlerSpec.m; sourceTree = "<group>"; };
4A457DD624A3CCED00386CBA /* SDLLifecycleSystemRequestHandlerSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLLifecycleSystemRequestHandlerSpec.m; path = DevAPISpecs/SDLLifecycleSystemRequestHandlerSpec.m; sourceTree = "<group>"; };
4A457DD824A5137100386CBA /* SDLLifecycleProtocolHandlerSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLLifecycleProtocolHandlerSpec.m; path = DevAPISpecs/SDLLifecycleProtocolHandlerSpec.m; sourceTree = "<group>"; };
+ 4A5822C125E40BB5002822F1 /* NSArray+ExtensionsSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "NSArray+ExtensionsSpec.m"; path = "DevAPISpecs/NSArray+ExtensionsSpec.m"; sourceTree = "<group>"; };
4A680F192513E1F4004A2C31 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = SOURCE_ROOT; };
4A8BD22324F93131000945E3 /* SDLMassageCushionFirmness.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDLMassageCushionFirmness.h; path = public/SDLMassageCushionFirmness.h; sourceTree = "<group>"; };
4A8BD22424F93131000945E3 /* SDLMediaServiceData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SDLMediaServiceData.m; path = public/SDLMediaServiceData.m; sourceTree = "<group>"; };
@@ -3595,6 +3601,10 @@
B3A9DA1125D270E900CDFD21 /* SDLKeyboardLayoutCapabilitySpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDLKeyboardLayoutCapabilitySpec.m; sourceTree = "<group>"; };
B3EC9E6D2579AA010039F3AA /* SDLClimateDataSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDLClimateDataSpec.m; sourceTree = "<group>"; };
BB3C600D221AEF37007DD4CA /* NSMutableDictionary+StoreSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "NSMutableDictionary+StoreSpec.m"; path = "DevAPISpecs/NSMutableDictionary+StoreSpec.m"; sourceTree = "<group>"; };
+ C9707D1625DEE786009D00F2 /* NSArray+Extensions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "NSArray+Extensions.h"; path = "private/NSArray+Extensions.h"; sourceTree = "<group>"; };
+ C9707D1725DEE786009D00F2 /* NSArray+Extensions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "NSArray+Extensions.m"; path = "private/NSArray+Extensions.m"; sourceTree = "<group>"; };
+ C9707D2E25E0444D009D00F2 /* SDLMacros.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDLMacros.h; path = private/SDLMacros.h; sourceTree = "<group>"; };
+ C9707D2F25E0444D009D00F2 /* SDLMacros.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLMacros.m; path = private/SDLMacros.m; sourceTree = "<group>"; };
C975877E257AEFDB0066F271 /* SDLSeekIndicatorTypeSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDLSeekIndicatorTypeSpec.m; sourceTree = "<group>"; };
C9758784257F4C570066F271 /* SDLSeekStreamingIndicatorSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDLSeekStreamingIndicatorSpec.m; sourceTree = "<group>"; };
C9DFFE76257ACE0000F7D57A /* SDLSeekStreamingIndicator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDLSeekStreamingIndicator.h; path = public/SDLSeekStreamingIndicator.h; sourceTree = "<group>"; };
@@ -5877,6 +5887,7 @@
children = (
5D6EB4CB1BF28DC600693731 /* NSMapTable+SubscriptingSpec.m */,
BB3C600D221AEF37007DD4CA /* NSMutableDictionary+StoreSpec.m */,
+ 4A5822C125E40BB5002822F1 /* NSArray+ExtensionsSpec.m */,
);
name = Categories;
sourceTree = "<group>";
@@ -6191,6 +6202,10 @@
4ABB24B824F592620061BF55 /* NSMutableDictionary+SafeRemove.h */,
4ABB24B524F592620061BF55 /* NSMutableDictionary+SafeRemove.m */,
4A8BD3AB24F98602000945E3 /* NSNumber+NumberType.h */,
+ C9707D1625DEE786009D00F2 /* NSArray+Extensions.h */,
+ C9707D1725DEE786009D00F2 /* NSArray+Extensions.m */,
+ C9707D2E25E0444D009D00F2 /* SDLMacros.h */,
+ C9707D2F25E0444D009D00F2 /* SDLMacros.m */,
);
name = Categories;
sourceTree = "<group>";
@@ -7357,6 +7372,7 @@
4ABB2AB824F847F40061BF55 /* SDLSendHapticDataResponse.h in Headers */,
4ABB268624F7F8E20061BF55 /* SDLMutableDataQueue.h in Headers */,
4ABB27CE24F8006D0061BF55 /* SDLMediaType.h in Headers */,
+ C9707D1825DEE786009D00F2 /* NSArray+Extensions.h in Headers */,
4ABB28E324F82A6A0061BF55 /* SDLOnTBTClientState.h in Headers */,
4ABB2A5C24F847B10061BF55 /* SDLGetWayPointsResponse.h in Headers */,
4ABB260024F7E9230061BF55 /* SDLStreamingMediaManagerConstants.h in Headers */,
@@ -7441,6 +7457,7 @@
4ABB25E024F7E7980061BF55 /* SDLStreamingMediaConfiguration.h in Headers */,
4A8BD2FE24F938A4000945E3 /* SDLVehicleType.h in Headers */,
4ABB282B24F824E70061BF55 /* SDLTBTState.h in Headers */,
+ C9707D3025E0444D009D00F2 /* SDLMacros.h in Headers */,
4ABB26D724F7FAFD0061BF55 /* SDLRPCMessage.h in Headers */,
4A8BD24A24F93135000945E3 /* SDLMyKey.h in Headers */,
4ABB24F924F5959E0061BF55 /* SDLAsynchronousOperation.h in Headers */,
@@ -7998,6 +8015,7 @@
4ABB255C24F7E5880061BF55 /* SDLPermissionFilter.m in Sources */,
4ABB27C624F8006D0061BF55 /* SDLModuleType.m in Sources */,
4ABB25FD24F7E8E10061BF55 /* SDLStreamingVideoScaleManager.m in Sources */,
+ C9707D3125E0444D009D00F2 /* SDLMacros.m in Sources */,
4ABB26F724F7FB8F0061BF55 /* SDLAudioStreamingState.m in Sources */,
4ABB265A24F7F5C10061BF55 /* SDLRPCResponseNotification.m in Sources */,
4ABB2AB724F847F40061BF55 /* SDLSetGlobalPropertiesResponse.m in Sources */,
@@ -8039,6 +8057,7 @@
4ABB265624F7F5B40061BF55 /* SDLRPCNotificationNotification.m in Sources */,
4ABB2B8424F8504A0061BF55 /* SDLHMISettingsControlData.m in Sources */,
4ABB287624F8294A0061BF55 /* SDLVentilationMode.m in Sources */,
+ C9707D1925DEE786009D00F2 /* NSArray+Extensions.m in Sources */,
4ABB25E724F7E7A90061BF55 /* SDLAudioStreamManager.m in Sources */,
4ABB277B24F7FEBA0061BF55 /* SDLLightStatus.m in Sources */,
4ABB2A7324F847D40061BF55 /* SDLPutFileResponse.m in Sources */,
@@ -8910,6 +8929,7 @@
5DBF0D601F3B3DB4008AF2C9 /* SDLControlFrameVideoStartServiceAckSpec.m in Sources */,
162E83311A9BDE8B00906325 /* SDLListFilesSpec.m in Sources */,
EE5D1B33208EBCA900D17216 /* SDLTCPTransportSpec.m in Sources */,
+ 4A5822C225E40BB5002822F1 /* NSArray+ExtensionsSpec.m in Sources */,
DA9F7EB01DCC063400ACAE48 /* SDLLocationDetailsSpec.m in Sources */,
8816772922208B82001FACFF /* SDLNavigationInstructionSpec.m in Sources */,
5DC978261B7A38640012C2F1 /* SDLGlobalsSpec.m in Sources */,
diff --git a/SmartDeviceLink/private/NSArray+Extensions.h b/SmartDeviceLink/private/NSArray+Extensions.h
new file mode 100644
index 000000000..d88bd5033
--- /dev/null
+++ b/SmartDeviceLink/private/NSArray+Extensions.h
@@ -0,0 +1,20 @@
+//
+// NSArray+Extensions.h
+// SmartDeviceLink
+//
+// Created by Frank Elias on 2/18/21.
+// Copyright © 2021 smartdevicelink. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import "SDLMacros.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface NSArray (Extensions)
+
+-(NSUInteger)dynamicHash;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/SmartDeviceLink/private/NSArray+Extensions.m b/SmartDeviceLink/private/NSArray+Extensions.m
new file mode 100644
index 000000000..c042e8ab3
--- /dev/null
+++ b/SmartDeviceLink/private/NSArray+Extensions.m
@@ -0,0 +1,26 @@
+//
+// NSArray+Extensions.m
+// SmartDeviceLink
+//
+// Created by Frank Elias on 2/18/21.
+// Copyright © 2021 smartdevicelink. All rights reserved.
+//
+
+#import "NSArray+Extensions.h"
+
+@implementation NSArray (Extensions)
+
+/// A dynamic version of the NSArray `hash` method. The default `hash` method returns the number of objects in the array as its hash and this leads to clashes between arrays with the same number of objects.
+/// Instead, this method calls hash on each of it's sub-objects, XOR and rotates them, then returns this as the hash. This is much slower than the default hash method, but sometimes necessary to properly compare arrays.
+/// @returns A hash based on the hashes of all of the contained objects
+- (NSUInteger)dynamicHash {
+ NSUInteger retVal = 0;
+ for (NSUInteger i = 0; i < self.count; i++) {
+ retVal ^= NSUIntRotateCell(((NSObject *)self[i]).hash, (NSUIntBitCell / (i + 2)));
+ }
+
+ return retVal;
+}
+
+
+@end
diff --git a/SmartDeviceLink/private/SDLChoiceSetManager.m b/SmartDeviceLink/private/SDLChoiceSetManager.m
index 926b7043d..1a44ef5de 100644
--- a/SmartDeviceLink/private/SDLChoiceSetManager.m
+++ b/SmartDeviceLink/private/SDLChoiceSetManager.m
@@ -38,6 +38,7 @@
#import "SDLStateMachine.h"
#import "SDLSystemCapability.h"
#import "SDLSystemCapabilityManager.h"
+#import "SDLVersion.h"
#import "SDLWindowCapability.h"
#import "SDLWindowCapability+ScreenManagerExtensions.h"
@@ -53,6 +54,7 @@ typedef NSNumber * SDLChoiceId;
@interface SDLChoiceCell()
@property (assign, nonatomic) UInt16 choiceId;
+@property (strong, nonatomic, readwrite) NSString *uniqueText;
@end
@@ -229,13 +231,14 @@ UInt16 const ChoiceCellCancelIdMax = 200;
return;
}
- NSMutableSet<SDLChoiceCell *> *choicesToUpload = [[self sdl_choicesToBeUploadedWithArray:choices] mutableCopy];
+ NSMutableOrderedSet<SDLChoiceCell *> *mutableChoicesToUpload = [self sdl_choicesToBeUploadedWithArray:choices];
[SDLGlobals runSyncOnSerialSubQueue:self.readWriteQueue block:^{
- [choicesToUpload minusSet:self.preloadedMutableChoices];
- [choicesToUpload minusSet:self.pendingMutablePreloadChoices];
+ [mutableChoicesToUpload minusSet:self.preloadedMutableChoices];
+ [mutableChoicesToUpload minusSet:self.pendingMutablePreloadChoices];
}];
+ NSOrderedSet<SDLChoiceCell *> *choicesToUpload = [mutableChoicesToUpload copy];
if (choicesToUpload.count == 0) {
SDLLogD(@"All choices already preloaded. No need to perform a preload");
if (handler != nil) {
@@ -249,7 +252,7 @@ UInt16 const ChoiceCellCancelIdMax = 200;
// Add the preload cells to the pending preloads
[SDLGlobals runSyncOnSerialSubQueue:self.readWriteQueue block:^{
- [self.pendingMutablePreloadChoices unionSet:choicesToUpload];
+ [self.pendingMutablePreloadChoices unionSet:choicesToUpload.set];
}];
// Upload pending preloads
@@ -277,8 +280,8 @@ UInt16 const ChoiceCellCancelIdMax = 200;
[SDLGlobals runSyncOnSerialSubQueue:self.readWriteQueue block:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
- [strongSelf.preloadedMutableChoices unionSet:choicesToUpload];
- [strongSelf.pendingMutablePreloadChoices minusSet:choicesToUpload];
+ [strongSelf.preloadedMutableChoices unionSet:choicesToUpload.set];
+ [strongSelf.pendingMutablePreloadChoices minusSet:choicesToUpload.set];
}];
};
[self.transactionQueue addOperation:preloadOp];
@@ -447,11 +450,38 @@ UInt16 const ChoiceCellCancelIdMax = 200;
/// Checks the passed list of choices to be uploaded and returns the items that have not yet been uploaded to the module.
/// @param choices The choices to be uploaded
/// @return The choices that have not yet been uploaded to the module
-- (NSSet<SDLChoiceCell *> *)sdl_choicesToBeUploadedWithArray:(NSArray<SDLChoiceCell *> *)choices {
- NSMutableSet<SDLChoiceCell *> *choicesSet = [NSMutableSet setWithArray:choices];
+- (NSMutableOrderedSet<SDLChoiceCell *> *)sdl_choicesToBeUploadedWithArray:(NSArray<SDLChoiceCell *> *)choices {
+ NSMutableOrderedSet<SDLChoiceCell *> *choicesSet = [[NSMutableOrderedSet alloc] initWithArray:choices];
+
+ // If we're running on a connection < RPC 7.1, we need to de-duplicate cells because presenting them will fail if we have the same cell primary text.
+ SDLVersion *choiceUniquenessSupportedVersion = [[SDLVersion alloc] initWithMajor:7 minor:1 patch:0];
+ if ([[SDLGlobals sharedGlobals].rpcVersion isLessThanVersion:choiceUniquenessSupportedVersion]) {
+ [self sdl_addUniqueNamesToCells:choicesSet];
+ }
[choicesSet minusSet:self.preloadedChoices];
- return [choicesSet copy];
+ return choicesSet;
+}
+
+/// Checks if 2 or more cells have the same text/title. In case this condition is true, this function will handle the presented issue by adding "(count)".
+/// E.g. Choices param contains 2 cells with text/title "Address" will be handled by updating the uniqueText/uniqueTitle of the second cell to "Address (2)".
+/// @param choices The choices to be uploaded.
+- (void)sdl_addUniqueNamesToCells:(NSOrderedSet<SDLChoiceCell *> *)choices {
+ // Tracks how many of each cell primary text there are so that we can append numbers to make each unique as necessary
+ NSMutableDictionary<NSString *, NSNumber *> *dictCounter = [[NSMutableDictionary alloc] init];
+ for (SDLChoiceCell *cell in choices) {
+ NSString *cellName = cell.text;
+ NSNumber *counter = dictCounter[cellName];
+ if (counter != nil) {
+ counter = @(counter.intValue + 1);
+ dictCounter[cellName] = counter;
+ } else {
+ dictCounter[cellName] = @1;
+ }
+ if (counter.intValue > 1) {
+ cell.uniqueText = [NSString stringWithFormat: @"%@ (%d)", cell.text, counter.intValue];
+ }
+ }
}
/// Checks the passed list of choices to be deleted and returns the items that have been uploaded to the module.
@@ -476,7 +506,7 @@ UInt16 const ChoiceCellCancelIdMax = 200;
/// Assigns a unique id to each choice item.
/// @param choices An array of choices
-- (void)sdl_updateIdsOnChoices:(NSSet<SDLChoiceCell *> *)choices {
+- (void)sdl_updateIdsOnChoices:(NSOrderedSet<SDLChoiceCell *> *)choices {
for (SDLChoiceCell *cell in choices) {
cell.choiceId = self.nextChoiceId;
}
diff --git a/SmartDeviceLink/private/SDLMacros.h b/SmartDeviceLink/private/SDLMacros.h
new file mode 100644
index 000000000..fff4ccbdf
--- /dev/null
+++ b/SmartDeviceLink/private/SDLMacros.h
@@ -0,0 +1,16 @@
+//
+// SDLMacros.h
+// SmartDeviceLink
+//
+// Created by Joel Fischer on 2/8/21.
+// Copyright © 2021 smartdevicelink. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+extern NSUInteger const NSUIntBitCell;
+extern NSUInteger NSUIntRotateCell(NSUInteger val, NSUInteger howMuch);
+
+NS_ASSUME_NONNULL_END
diff --git a/SmartDeviceLink/private/SDLMacros.m b/SmartDeviceLink/private/SDLMacros.m
new file mode 100644
index 000000000..ed92ea8e5
--- /dev/null
+++ b/SmartDeviceLink/private/SDLMacros.m
@@ -0,0 +1,14 @@
+//
+// SDLMacros.m
+// SmartDeviceLink
+//
+// Created by Joel Fischer on 2/8/21.
+// Copyright © 2021 smartdevicelink. All rights reserved.
+//
+
+#import "SDLMacros.h"
+
+NSUInteger const NSUIntBitCell = (CHAR_BIT * sizeof(NSUInteger));
+NSUInteger NSUIntRotateCell(NSUInteger val, NSUInteger howMuch) {
+ return ((((NSUInteger)val) << howMuch) | (((NSUInteger)val) >> (NSUIntBitCell - howMuch)));
+}
diff --git a/SmartDeviceLink/private/SDLMenuManager.m b/SmartDeviceLink/private/SDLMenuManager.m
index a45d171f2..383b3f220 100644
--- a/SmartDeviceLink/private/SDLMenuManager.m
+++ b/SmartDeviceLink/private/SDLMenuManager.m
@@ -48,6 +48,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (assign, nonatomic) UInt32 parentCellId;
@property (assign, nonatomic) UInt32 cellId;
+@property (strong, nonatomic, readwrite) NSString *uniqueTitle;
@end
@@ -160,6 +161,9 @@ UInt32 const MenuCellIdMin = 1;
}
- (void)setMenuCells:(NSArray<SDLMenuCell *> *)menuCells {
+ // Check for cell lists with completely duplicate information, or any duplicate voiceCommands and return if it fails (logs are in the called method).
+ if (![self sdl_menuCellsAreUnique:menuCells allVoiceCommands:[NSMutableArray array]]) { return; }
+
if (self.currentHMILevel == nil
|| [self.currentHMILevel isEqualToEnum:SDLHMILevelNone]
|| [self.currentSystemContext isEqualToEnum:SDLSystemContextMenu]) {
@@ -171,26 +175,10 @@ UInt32 const MenuCellIdMin = 1;
self.waitingOnHMIUpdate = NO;
- NSMutableSet *titleCheckSet = [NSMutableSet set];
- NSMutableSet<NSString *> *allMenuVoiceCommands = [NSMutableSet set];
- NSUInteger voiceCommandCount = 0;
- for (SDLMenuCell *cell in menuCells) {
- [titleCheckSet addObject:cell.title];
- if (cell.voiceCommands == nil) { continue; }
- [allMenuVoiceCommands addObjectsFromArray:cell.voiceCommands];
- voiceCommandCount += cell.voiceCommands.count;
- }
-
- // Check for duplicate titles
- if (titleCheckSet.count != menuCells.count) {
- SDLLogE(@"Not all cell titles are unique. The menu will not be set.");
- return;
- }
-
- // Check for duplicate voice recognition commands
- if (allMenuVoiceCommands.count != voiceCommandCount) {
- SDLLogE(@"Attempted to create a menu with duplicate voice commands. Voice commands must be unique. The menu will not be set.");
- return;
+ // If connected over RPC < 7.1, append unique identifiers to cell titles that are duplicates even if other properties are identical
+ SDLVersion *menuUniquenessSupportedVersion = [[SDLVersion alloc] initWithMajor:7 minor:1 patch:0];
+ if ([[SDLGlobals sharedGlobals].rpcVersion isLessThanVersion:menuUniquenessSupportedVersion]) {
+ [self sdl_addUniqueNamesToCells:menuCells];
}
_oldMenuCells = _menuCells;
@@ -212,7 +200,6 @@ UInt32 const MenuCellIdMin = 1;
}
SDLShowAppMenu *openMenu = [[SDLShowAppMenu alloc] init];
-
[self.connectionManager sendConnectionRequest:openMenu withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) {
if ([response.resultCode isEqualToEnum:SDLResultWarnings]) {
SDLLogW(@"Warning opening application menu: %@", error);
@@ -239,7 +226,6 @@ UInt32 const MenuCellIdMin = 1;
}
SDLShowAppMenu *subMenu = [[SDLShowAppMenu alloc] initWithMenuID:cell.cellId];
-
[self.connectionManager sendConnectionRequest:subMenu withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) {
if ([response.resultCode isEqualToEnum:SDLResultWarnings]) {
SDLLogW(@"Warning opening application menu to submenu cell %@, with error: %@", cell, error);
@@ -530,6 +516,71 @@ UInt32 const MenuCellIdMin = 1;
}
}
+/// Checks if 2 or more cells have the same text/title. In case this condition is true, this function will handle the presented issue by adding "(count)".
+/// E.g. Choices param contains 2 cells with text/title "Address" will be handled by updating the uniqueText/uniqueTitle of the second cell to "Address (2)".
+/// @param choices The choices to be uploaded.
+- (void)sdl_addUniqueNamesToCells:(nullable NSArray<SDLMenuCell *> *)choices {
+ // Tracks how many of each cell primary text there are so that we can append numbers to make each unique as necessary
+ NSMutableDictionary<NSString *, NSNumber *> *dictCounter = [[NSMutableDictionary alloc] init];
+ for (SDLMenuCell *cell in choices) {
+ NSString *cellName = cell.title;
+ NSNumber *counter = dictCounter[cellName];
+ if (counter != nil) {
+ counter = @(counter.intValue + 1);
+ dictCounter[cellName] = counter;
+ } else {
+ dictCounter[cellName] = @1;
+ }
+
+ counter = dictCounter[cellName];
+ if (counter.intValue > 1) {
+ cell.uniqueTitle = [NSString stringWithFormat: @"%@ (%d)", cell.title, counter.intValue];
+ }
+
+ if (cell.subCells.count > 0) {
+ [self sdl_addUniqueNamesToCells:cell.subCells];
+ }
+ }
+}
+
+/**
+ Check for cell lists with completely duplicate information, or any duplicate voiceCommands
+
+ @param cells The cells you will be adding
+ @return Boolean that indicates whether menuCells are unique or not
+ */
+- (BOOL)sdl_menuCellsAreUnique:(NSArray<SDLMenuCell *> *)cells allVoiceCommands:(NSMutableArray<NSString *> *)allVoiceCommands {
+ ///Check all voice commands for identical items and check each list of cells for identical cells
+ NSMutableSet<SDLMenuCell *> *identicalCellsCheckSet = [NSMutableSet set];
+ for (SDLMenuCell *cell in cells) {
+ [identicalCellsCheckSet addObject:cell];
+
+ // Recursively check the subcell lists to see if they are all unique as well. If anything is not, this will chain back up the list to return false.
+ if (cell.subCells.count > 0) {
+ BOOL subcellsAreUnique = [self sdl_menuCellsAreUnique:cell.subCells allVoiceCommands:allVoiceCommands];
+ if (!subcellsAreUnique) { return NO; }
+ }
+
+ // Voice commands have to be identical across all lists
+ if (cell.voiceCommands == nil) { continue; }
+ [allVoiceCommands addObjectsFromArray:cell.voiceCommands];
+ }
+
+ // Check for duplicate cells
+ if (identicalCellsCheckSet.count != cells.count) {
+ SDLLogE(@"Not all cells are unique. Cells in each list (such as main menu or subcell list) must have some differentiating property other than the subcells within a cell. The menu will not be set.");
+ return NO;
+ }
+
+ // All the VR commands must be unique
+ if (allVoiceCommands.count != [NSSet setWithArray:allVoiceCommands].count) {
+ SDLLogE(@"Attempted to create a menu with duplicate voice commands, but voice commands must be unique across all menu items including main menu and subcell lists. The menu will not be set.");
+ return NO;
+ }
+
+ return YES;
+}
+
#pragma mark Artworks
- (NSArray<SDLArtwork *> *)sdl_findAllArtworksToBeUploadedFromCells:(NSArray<SDLMenuCell *> *)cells {
@@ -670,7 +721,7 @@ UInt32 const MenuCellIdMin = 1;
SDLAddCommand *command = [[SDLAddCommand alloc] init];
SDLMenuParams *params = [[SDLMenuParams alloc] init];
- params.menuName = cell.title;
+ params.menuName = cell.uniqueTitle;
params.parentID = cell.parentCellId != UINT32_MAX ? @(cell.parentCellId) : nil;
params.position = @(position);
params.secondaryText = (cell.secondaryText.length == 0) ? nil : cell.secondaryText;
@@ -698,8 +749,7 @@ UInt32 const MenuCellIdMin = 1;
NSString *secondaryText = (cell.secondaryText.length == 0) ? nil : cell.secondaryText;
NSString *tertiaryText = (cell.tertiaryText.length == 0) ? nil : cell.tertiaryText;
-
- return [[SDLAddSubMenu alloc] initWithMenuID:cell.cellId menuName:cell.title position:@(position) menuIcon:icon menuLayout:submenuLayout parentID:nil secondaryText:secondaryText tertiaryText:tertiaryText secondaryImage:secondaryImage];
+ return [[SDLAddSubMenu alloc] initWithMenuID:cell.cellId menuName:cell.uniqueTitle position:@(position) menuIcon:icon menuLayout:submenuLayout parentID:nil secondaryText:secondaryText tertiaryText:tertiaryText secondaryImage:secondaryImage];
}
#pragma mark - Calling handlers
diff --git a/SmartDeviceLink/private/SDLPreloadChoicesOperation.h b/SmartDeviceLink/private/SDLPreloadChoicesOperation.h
index 965a0c176..d13207705 100644
--- a/SmartDeviceLink/private/SDLPreloadChoicesOperation.h
+++ b/SmartDeviceLink/private/SDLPreloadChoicesOperation.h
@@ -29,7 +29,7 @@ typedef NS_ENUM(NSUInteger, SDLPreloadChoicesOperationState) {
@property (assign, nonatomic) SDLPreloadChoicesOperationState currentState;
-- (instancetype)initWithConnectionManager:(id<SDLConnectionManagerType>)connectionManager fileManager:(SDLFileManager *)fileManager displayName:(NSString *)displayName windowCapability:(SDLWindowCapability *)defaultMainWindowCapability isVROptional:(BOOL)isVROptional cellsToPreload:(NSSet<SDLChoiceCell *> *)cells;
+- (instancetype)initWithConnectionManager:(id<SDLConnectionManagerType>)connectionManager fileManager:(SDLFileManager *)fileManager displayName:(NSString *)displayName windowCapability:(SDLWindowCapability *)defaultMainWindowCapability isVROptional:(BOOL)isVROptional cellsToPreload:(NSOrderedSet<SDLChoiceCell *> *)cells;
- (BOOL)removeChoicesFromUpload:(NSSet<SDLChoiceCell *> *)choices;
diff --git a/SmartDeviceLink/private/SDLPreloadChoicesOperation.m b/SmartDeviceLink/private/SDLPreloadChoicesOperation.m
index b52a82f70..1390169c6 100644
--- a/SmartDeviceLink/private/SDLPreloadChoicesOperation.m
+++ b/SmartDeviceLink/private/SDLPreloadChoicesOperation.m
@@ -45,7 +45,7 @@ NS_ASSUME_NONNULL_BEGIN
@implementation SDLPreloadChoicesOperation
-- (instancetype)initWithConnectionManager:(id<SDLConnectionManagerType>)connectionManager fileManager:(SDLFileManager *)fileManager displayName:(NSString *)displayName windowCapability:(SDLWindowCapability *)defaultMainWindowCapability isVROptional:(BOOL)isVROptional cellsToPreload:(NSSet<SDLChoiceCell *> *)cells {
+- (instancetype)initWithConnectionManager:(id<SDLConnectionManagerType>)connectionManager fileManager:(SDLFileManager *)fileManager displayName:(NSString *)displayName windowCapability:(SDLWindowCapability *)defaultMainWindowCapability isVROptional:(BOOL)isVROptional cellsToPreload:(NSOrderedSet<SDLChoiceCell *> *)cells {
self = [super init];
if (!self) { return nil; }
@@ -160,7 +160,7 @@ NS_ASSUME_NONNULL_BEGIN
NSString *menuName = nil;
if ([self sdl_shouldSendChoiceText]) {
- menuName = cell.text;
+ menuName = cell.uniqueText;
}
if(!menuName) {
diff --git a/SmartDeviceLink/public/SDLChoiceCell.h b/SmartDeviceLink/public/SDLChoiceCell.h
index 197f0190d..34a80461d 100644
--- a/SmartDeviceLink/public/SDLChoiceCell.h
+++ b/SmartDeviceLink/public/SDLChoiceCell.h
@@ -46,6 +46,10 @@ NS_ASSUME_NONNULL_BEGIN
@property (strong, nonatomic, readonly, nullable) SDLArtwork *secondaryArtwork;
/**
+ Primary text of the cell to be displayed on the module. Used to distinguish cells with the same `text` but other fields are different. This is autogenerated by the screen manager. Attempting to use cells that are exactly the same (all text and artwork fields are the same) will not cause this to be used. This will not be used when connected to modules supporting RPC 7.1+.
+ */
+@property (strong, nonatomic, readonly) NSString *uniqueText;
+/**
Initialize the cell with nothing. This is unavailable
@return A crash, probably
diff --git a/SmartDeviceLink/public/SDLChoiceCell.m b/SmartDeviceLink/public/SDLChoiceCell.m
index 05ceb4daa..c62d39272 100644
--- a/SmartDeviceLink/public/SDLChoiceCell.m
+++ b/SmartDeviceLink/public/SDLChoiceCell.m
@@ -9,12 +9,14 @@
#import "SDLChoiceCell.h"
#import "SDLArtwork.h"
+#import "NSArray+Extensions.h"
NS_ASSUME_NONNULL_BEGIN
@interface SDLChoiceCell()
@property (assign, nonatomic) UInt16 choiceId;
+@property (nonatomic, readwrite) NSString *uniqueText;
@end
@@ -40,6 +42,7 @@ NS_ASSUME_NONNULL_BEGIN
_voiceCommands = voiceCommands;
_artwork = artwork;
_secondaryArtwork = secondaryArtwork;
+ _uniqueText = text;
_choiceId = UINT16_MAX;
@@ -65,7 +68,7 @@ NSUInteger NSUIntRotate(NSUInteger val, NSUInteger howMuch) {
^ NSUIntRotate(self.tertiaryText.hash, NSUIntBit / 4)
^ NSUIntRotate(self.artwork.name.hash, NSUIntBit / 5)
^ NSUIntRotate(self.secondaryArtwork.name.hash, NSUIntBit / 6)
- ^ NSUIntRotate(self.voiceCommands.hash, NSUIntBit / 7);
+ ^ NSUIntRotate(self.voiceCommands.dynamicHash, NSUIntBit / 7);
}
- (BOOL)isEqual:(id)object {
@@ -84,7 +87,7 @@ NSUInteger NSUIntRotate(NSUInteger val, NSUInteger howMuch) {
#pragma mark - Etc.
- (NSString *)description {
- return [NSString stringWithFormat:@"SDLChoiceCell: %u-\"%@ - %@ - %@\", artworkNames: %@ - %@, voice commands: %lu", _choiceId, _text, _secondaryText, _tertiaryText, _artwork.name, _secondaryArtwork.name, (unsigned long)_voiceCommands.count];
+ return [NSString stringWithFormat:@"SDLChoiceCell: %u-\"%@ - %@ - %@\", artworkNames: %@ - %@, voice commands: %lu, uniqueText: %@", _choiceId, _text, _secondaryText, _tertiaryText, _artwork.name, _secondaryArtwork.name, (unsigned long)_voiceCommands.count, ([_text isEqualToString:_uniqueText] ? @"NO" : _uniqueText)];
}
@end
diff --git a/SmartDeviceLink/public/SDLChoiceSet.h b/SmartDeviceLink/public/SDLChoiceSet.h
index 674033553..1eafb8bd8 100644
--- a/SmartDeviceLink/public/SDLChoiceSet.h
+++ b/SmartDeviceLink/public/SDLChoiceSet.h
@@ -93,9 +93,20 @@ typedef NS_ENUM(NSUInteger, SDLChoiceSetLayout) {
*/
@property (copy, nonatomic) NSArray<SDLChoiceCell *> *choices;
+/// Initialize with a title, delegate, and choices. It will use the default timeout and layout, all other properties (such as prompts) will be `nil`.
+///
+/// WARNING: If you display multiple cells with the same `text` with the only uniquing property between cells being different `vrCommands` or a feature that is not displayed on the head unit (e.g. if the head unit doesn't display `secondaryArtwork` and that's the only uniquing property between two cells) then the cells may appear to be the same to the user in `Manual` mode. This only applies to RPC connections >= 7.1.0.
+///
+/// WARNING: On < 7.1.0 connections, the `text` cell will be automatically modified among cells that have the same `text` when they are preloaded, so they will always appear differently on-screen when they are displayed. `cell.uniqueText` will be created by appending ` (2)`, ` (3)`, etc.
+- (instancetype)init;
+
/**
Initialize with a title, delegate, and choices. It will use the default timeout and layout, all other properties (such as prompts) will be `nil`.
+ WARNING: If you display multiple cells with the same `text` with the only uniquing property between cells being different `vrCommands` or a feature that is not displayed on the head unit (e.g. if the head unit doesn't display `secondaryArtwork` and that's the only uniquing property between two cells) then the cells may appear to be the same to the user in `Manual` mode. This only applies to RPC connections >= 7.1.0.
+
+ WARNING: On < 7.1.0 connections, the `text` cell will be automatically modified among cells that have the same `text` when they are preloaded, so they will always appear differently on-screen when they are displayed. `cell.uniqueText` will be created by appending ` (2)`, ` (3)`, etc.
+
@param title The choice set's title
@param delegate The choice set delegate called after the user has interacted with your choice set
@param choices The choices to be displayed to the user for interaction
@@ -106,6 +117,10 @@ typedef NS_ENUM(NSUInteger, SDLChoiceSetLayout) {
/**
Initializer with all possible properties.
+ WARNING: If you display multiple cells with the same `text` with the only uniquing property between cells being different `vrCommands` or a feature that is not displayed on the head unit (e.g. if the head unit doesn't display `secondaryArtwork` and that's the only uniquing property between two cells) then the cells may appear to be the same to the user in `Manual` mode. This only applies to RPC connections >= 7.1.0.
+
+ WARNING: On < 7.1.0 connections, the `text` cell will be automatically modified among cells that have the same `text` when they are preloaded, so they will always appear differently on-screen when they are displayed. `cell.uniqueText` will be created by appending ` (2)`, ` (3)`, etc.
+
@param title The choice set's title
@param delegate The choice set delegate called after the user has interacted with your choice set
@param layout The layout of choice options (Manual/touch only)
@@ -122,6 +137,10 @@ typedef NS_ENUM(NSUInteger, SDLChoiceSetLayout) {
/**
Initializer with all possible properties.
+ WARNING: If you display multiple cells with the same `text` with the only uniquing property between cells being different `vrCommands` or a feature that is not displayed on the head unit (e.g. if the head unit doesn't display `secondaryArtwork` and that's the only uniquing property between two cells) then the cells may appear to be the same to the user in `Manual` mode. This only applies to RPC connections >= 7.1.0.
+
+ WARNING: On < 7.1.0 connections, the `text` cell will be automatically modified among cells that have the same `text` when they are preloaded, so they will always appear differently on-screen when they are displayed. `cell.uniqueText` will be created by appending ` (2)`, ` (3)`, etc.
+
@param title The choice set's title
@param delegate The choice set delegate called after the user has interacted with your choice set
@param layout The layout of choice options (Manual/touch only)
diff --git a/SmartDeviceLink/public/SDLChoiceSet.m b/SmartDeviceLink/public/SDLChoiceSet.m
index 45a830bcd..775751381 100644
--- a/SmartDeviceLink/public/SDLChoiceSet.m
+++ b/SmartDeviceLink/public/SDLChoiceSet.m
@@ -12,6 +12,8 @@
#import "SDLLogMacros.h"
#import "SDLTTSChunk.h"
#import "SDLVrHelpItem.h"
+#import "SDLVersion.h"
+#import "SDLGlobals.h"
NS_ASSUME_NONNULL_BEGIN
@@ -67,32 +69,7 @@ static SDLChoiceSetLayout _defaultLayout = SDLChoiceSetLayoutList;
return nil;
}
- NSMutableSet<NSString *> *choiceTextSet = [NSMutableSet setWithCapacity:choices.count];
- NSMutableSet<NSString *> *uniqueVoiceCommands = [NSMutableSet set];
- NSUInteger allVoiceCommandsCount = 0;
- NSUInteger choiceCellWithVoiceCommandCount = 0;
- for (SDLChoiceCell *cell in choices) {
- [choiceTextSet addObject:cell.text];
- if (cell.voiceCommands == nil) { continue; }
- [uniqueVoiceCommands addObjectsFromArray:cell.voiceCommands];
- choiceCellWithVoiceCommandCount += 1;
- allVoiceCommandsCount += cell.voiceCommands.count;
- }
- if (choiceTextSet.count < choices.count) {
- SDLLogE(@"Attempted to create a choice set with duplicate cell text. Cell text must be unique. The choice set will not be set.");
- return nil;
- }
-
- // All or none of the choices must have VR commands
- if ((choiceCellWithVoiceCommandCount > 0 && choiceCellWithVoiceCommandCount < choices.count)) {
- SDLLogE(@"If using voice recognition commands, all of the choice set cells must have unique VR commands. There are %lu cells with unique voice commands and %lu total cells. The choice set will not be set.", (unsigned long)choiceCellWithVoiceCommandCount, (unsigned long)choices.count);
- return nil;
- }
- // All the VR commands must be unique
- if (uniqueVoiceCommands.count < allVoiceCommandsCount) {
- SDLLogE(@"If using voice recognition commands, all VR commands must be unique. There are %lu unique VR commands and %lu VR commands. The choice set will not be set.", (unsigned long)uniqueVoiceCommands.count, (unsigned long)allVoiceCommandsCount);
- return nil;
- }
+ if (![self sdl_choiceCellsAreUnique:choices]) { return nil; }
for (NSUInteger i = 0; i < helpList.count; i++) {
helpList[i].position = @(i + 1);
@@ -144,6 +121,40 @@ static SDLChoiceSetLayout _defaultLayout = SDLChoiceSetLayoutList;
}
}
+#pragma mark - Helpers
+
+/**
+ Check for duplicate choices and voiceCommands
+
+ @param choices The choices you will be adding
+ @return Boolean that indicates whether choices and voice commands are unique or not
+ */
+-(BOOL)sdl_choiceCellsAreUnique:(NSArray<SDLChoiceCell *> *)choices {
+ NSMutableSet<SDLChoiceCell *> *identicalCellsCheckSet = [NSMutableSet setWithCapacity:choices.count];
+ NSMutableSet<NSString *> *identicalVoiceCommandsCheckSet = [NSMutableSet set];
+ NSUInteger allVoiceCommandsCount = 0;
+ for (SDLChoiceCell *cell in choices) {
+ [identicalCellsCheckSet addObject:cell];
+
+ if (cell.voiceCommands == nil) { continue; }
+ [identicalVoiceCommandsCheckSet addObjectsFromArray:cell.voiceCommands];
+ allVoiceCommandsCount += cell.voiceCommands.count;
+ }
+
+ if (identicalCellsCheckSet.count < choices.count) {
+ SDLLogE(@"Attempted to create a choice set with duplicate cells. At least one property must be different between any two cells. The choice set will not be set.");
+ return NO;
+ }
+
+ // All the VR commands must be unique
+ if (identicalVoiceCommandsCheckSet.count < allVoiceCommandsCount) {
+ SDLLogE(@"Attempted to create a choice set where the cells contained duplicate voice commands. All VR commands must be unique. There are %lu unique VR commands and %lu VR commands. The choice set will not be set.", (unsigned long)identicalVoiceCommandsCheckSet.count, (unsigned long)allVoiceCommandsCount);
+ return NO;
+ }
+
+ return YES;
+}
+
#pragma mark - Etc.
- (NSString *)description {
diff --git a/SmartDeviceLink/public/SDLFile.m b/SmartDeviceLink/public/SDLFile.m
index 1efe7454b..b3efaf200 100644
--- a/SmartDeviceLink/public/SDLFile.m
+++ b/SmartDeviceLink/public/SDLFile.m
@@ -167,7 +167,7 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - NSObject overrides
- (NSString *)description {
- return [NSString stringWithFormat:@"SDLFile: %@", self.name];
+ return [NSString stringWithFormat:@"SDLFile: %@, isPersistent: %@, should overwrite: %@, is static icon: %@, file type: %@", self.name, (self.isPersistent ? @"YES" : @"NO"), (self.overwrite ? @"YES" : @"NO"), (self.isStaticIcon ? @"YES" : @"NO"), self.fileType];
}
- (NSUInteger)hash {
diff --git a/SmartDeviceLink/public/SDLMenuCell.h b/SmartDeviceLink/public/SDLMenuCell.h
index 3c32d70e0..f8383e44c 100644
--- a/SmartDeviceLink/public/SDLMenuCell.h
+++ b/SmartDeviceLink/public/SDLMenuCell.h
@@ -56,6 +56,11 @@ typedef void(^SDLMenuCellSelectionHandler)(SDLTriggerSource triggerSource);
@property (strong, nonatomic, readonly, nullable) SDLMenuLayout submenuLayout;
/**
+ Primary text of the cell to be displayed on the module. Used to distinguish cells with the same `title` but other fields are different. This is autogenerated by the screen manager. This will not be used when connected to modules supporting RPC 7.1+ because duplicate titles are supported.
+ */
+@property (strong, nonatomic, readonly) NSString *uniqueTitle;
+
+/**
The cell's secondary text to be displayed
*/
@property (copy, nonatomic, readonly, nullable) NSString *secondaryText;
diff --git a/SmartDeviceLink/public/SDLMenuCell.m b/SmartDeviceLink/public/SDLMenuCell.m
index e5bc7f13e..b9f0a7be4 100644
--- a/SmartDeviceLink/public/SDLMenuCell.m
+++ b/SmartDeviceLink/public/SDLMenuCell.m
@@ -9,6 +9,7 @@
#import "SDLMenuCell.h"
#import "SDLArtwork.h"
+#import "NSArray+Extensions.h"
NS_ASSUME_NONNULL_BEGIN
@@ -16,6 +17,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (assign, nonatomic) UInt32 parentCellId;
@property (assign, nonatomic) UInt32 cellId;
+@property (strong, nonatomic, readwrite) NSString *uniqueTitle;
@end
@@ -37,6 +39,7 @@ NS_ASSUME_NONNULL_BEGIN
_icon = icon;
_voiceCommands = voiceCommands;
_handler = handler;
+ _uniqueTitle = title;
_cellId = UINT32_MAX;
_parentCellId = UINT32_MAX;
@@ -57,6 +60,7 @@ NS_ASSUME_NONNULL_BEGIN
_submenuLayout = layout;
_icon = icon;
_subCells = subCells;
+ _uniqueTitle = title;
_cellId = UINT32_MAX;
_parentCellId = UINT32_MAX;
@@ -69,21 +73,16 @@ NS_ASSUME_NONNULL_BEGIN
}
- (NSString *)description {
- return [NSString stringWithFormat:@"SDLMenuCell: %u-\"%@\", artworkName: %@, voice commands: %lu, isSubcell: %@, hasSubcells: %@, submenuLayout: %@", (unsigned int)_cellId, _title, _icon.name, (unsigned long)_voiceCommands.count, (_parentCellId != UINT32_MAX ? @"YES" : @"NO"), (_subCells.count > 0 ? @"YES" : @"NO"), _submenuLayout];
+ return [NSString stringWithFormat:@"SDLMenuCell: %u-\"%@\", unique title: %@, artworkName: %@, voice commands: %lu, isSubcell: %@, hasSubcells: %@, submenuLayout: %@", (unsigned int)_cellId, _title, ([_title isEqualToString:_uniqueTitle] ? @"NO" : _uniqueTitle), _icon.name, (unsigned long)_voiceCommands.count, (_parentCellId != UINT32_MAX ? @"YES" : @"NO"), (_subCells.count > 0 ? @"YES" : @"NO"), _submenuLayout];
}
#pragma mark - Object Equality
-NSUInteger const NSUIntBitCell = (CHAR_BIT * sizeof(NSUInteger));
-NSUInteger NSUIntRotateCell(NSUInteger val, NSUInteger howMuch) {
- return ((((NSUInteger)val) << howMuch) | (((NSUInteger)val) >> (NSUIntBitCell - howMuch)));
-}
-
- (NSUInteger)hash {
return NSUIntRotateCell(self.title.hash, NSUIntBitCell / 2)
^ NSUIntRotateCell(self.icon.name.hash, NSUIntBitCell / 3)
- ^ NSUIntRotateCell(self.voiceCommands.hash, NSUIntBitCell / 4)
- ^ NSUIntRotateCell(self.subCells.count !=0, NSUIntBitCell / 5)
+ ^ NSUIntRotateCell(self.voiceCommands.dynamicHash, NSUIntBitCell / 4)
+ ^ NSUIntRotateCell((self.subCells.count != 0), NSUIntBitCell / 5)
^ NSUIntRotateCell(self.secondaryText.hash, NSUIntBitCell / 6)
^ NSUIntRotateCell(self.tertiaryText.hash, NSUIntBitCell / 7)
^ NSUIntRotateCell(self.secondaryArtwork.name.hash, NSUIntBitCell / 8)
diff --git a/SmartDeviceLink/public/SDLScreenManager.h b/SmartDeviceLink/public/SDLScreenManager.h
index c2da90750..c941eaced 100644
--- a/SmartDeviceLink/public/SDLScreenManager.h
+++ b/SmartDeviceLink/public/SDLScreenManager.h
@@ -183,6 +183,12 @@ typedef void (^SDLSubscribeButtonHandler)(SDLOnButtonPress *_Nullable buttonPres
/**
The current list of menu cells displayed in the app's menu.
+
+ WARNING: If two or more cells in this array are duplicates – they contain all of the same data – the menu will not be set. Each list of `subCells` and the main menu are compared separately, which means you can have duplicate cells between the main menu and a sub cell list without a conflict occurring.
+
+ WARNING: If two or more cells contain the same `title` but are otherwise distinctive, unique identifiers will be appended in the style (2), (3), (4), etc. to those cells' `title`. The same rules apply to duplicate titles as apply to complete duplicates above: the titles can be duplicates between different array lists without a conflict.
+
+ WARNING: If any two cells contain the same voice command string in their `voiceCommands` list, the menu will not be set. Note that unlike the two warnings above, these lists *are not* checked separately. If you have the same voice command in a cell of the main menu and a sub cell, it will not be set.
*/
@property (copy, nonatomic) NSArray<SDLMenuCell *> *menu;
@@ -191,7 +197,7 @@ Change the mode of the dynamic menu updater to be enabled, disabled, or enabled
The current status for dynamic menu updates. A dynamic menu update allows for smarter building of menu changes. If this status is set to `SDLDynamicMenuUpdatesModeForceOn`, menu updates will only create add commands for new items and delete commands for items no longer appearing in the menu. This helps reduce possible RPCs failures as there will be significantly less commands sent to the HMI.
-If set to `SDLDynamicMenuUpdatesModeForceOff`, menu updates will work the legacy way. This means when a new menu is set the entire old menu is deleted and add commands are created for every item regarldess if the item appears in both the old and new menu. This method is RPCs heavy and may cause some failures when creating and updating large menus.
+If set to `SDLDynamicMenuUpdatesModeForceOff`, menu updates will work the legacy way. This means when a new menu is set the entire old menu is deleted and add commands are created for every item regardless if the item appears in both the old and new menu. This method is RPCs heavy and may cause some failures when creating and updating large menus.
We recommend using either `SDLDynamicMenuUpdatesModeOnWithCompatibility` or `SDLDynamicMenuUpdatesModeForceOn`. `SDLDynamicMenuUpdatesModeOnWithCompatibility` turns dynamic updates off for head units that we know have poor compatibility with dynamic updates (e.g. they have bugs that cause menu items to not be placed correctly).
*/
diff --git a/SmartDeviceLinkTests/DevAPISpecs/NSArray+ExtensionsSpec.m b/SmartDeviceLinkTests/DevAPISpecs/NSArray+ExtensionsSpec.m
new file mode 100644
index 000000000..7a8bf1277
--- /dev/null
+++ b/SmartDeviceLinkTests/DevAPISpecs/NSArray+ExtensionsSpec.m
@@ -0,0 +1,60 @@
+//
+// NSArray+ExtensionsSpec.m
+// SmartDeviceLinkTests
+//
+// Created by Joel Fischer on 2/22/21.
+// Copyright © 2021 smartdevicelink. All rights reserved.
+//
+
+#import "NSArray+Extensions.h"
+
+#import <Quick/Quick.h>
+#import <Nimble/Nimble.h>
+
+QuickSpecBegin(NSArray_ExtensionsSpec)
+
+describe(@"checking the dynamic hash of an array", ^{
+ __block NSArray *testArray = nil;
+
+ beforeEach(^{
+ testArray = nil;
+ });
+
+ context(@"when the array has no objects", ^{
+ beforeEach(^{
+ testArray = @[];
+ });
+
+ it(@"should return a dynamic hash of 0", ^{
+ expect(testArray.dynamicHash).to(equal(0));
+ });
+ });
+
+ context(@"when the array contains one string", ^{
+ beforeEach(^{
+ testArray = @[@"test string"];
+ });
+
+ it(@"should return a consistent dynamic hash", ^{
+ expect(testArray.dynamicHash).to(equal(testArray.dynamicHash));
+ });
+
+ it(@"should return a different hash than the normal hash function", ^{
+ expect(testArray.dynamicHash).toNot(equal(testArray.hash));
+ });
+ });
+
+ context(@"when the array contains multiple strings", ^{
+ it(@"should return different numbers depending on where the strings are in the array", ^{
+ testArray = @[@"test string", @"test string 2"];
+ NSUInteger hash1 = testArray.dynamicHash;
+
+ testArray = @[@"test string 2", @"test string"];
+ NSUInteger hash2 = testArray.dynamicHash;
+
+ expect(hash1).toNot(equal(hash2));
+ });
+ });
+});
+
+QuickSpecEnd
diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLChoiceCellSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLChoiceCellSpec.m
index 1f4b6d469..47b6c675c 100644
--- a/SmartDeviceLinkTests/DevAPISpecs/SDLChoiceCellSpec.m
+++ b/SmartDeviceLinkTests/DevAPISpecs/SDLChoiceCellSpec.m
@@ -43,6 +43,7 @@ describe(@"an SDLChoiceCell", ^{
expect(testCell.artwork).to(beNil());
expect(testCell.secondaryArtwork).to(beNil());
expect(@(testCell.choiceId)).to(equal(@(UINT16_MAX)));
+ expect(testCell.uniqueText).to(equal(testText));
});
it(@"should initialize properly with initWithText:artwork:voiceCommands:", ^{
@@ -55,6 +56,7 @@ describe(@"an SDLChoiceCell", ^{
expect(testCell.artwork).to(equal(testArtwork));
expect(testCell.secondaryArtwork).to(beNil());
expect(@(testCell.choiceId)).to(equal(@(UINT16_MAX)));
+ expect(testCell.uniqueText).to(equal(testText));
});
it(@"should initialize properly with initWithText:secondaryText:tertiaryText:voiceCommands:artwork:secondaryArtwork:", ^{
@@ -67,6 +69,7 @@ describe(@"an SDLChoiceCell", ^{
expect(testCell.artwork).to(equal(testArtwork));
expect(testCell.secondaryArtwork).to(equal(testSecondaryArtwork));
expect(@(testCell.choiceId)).to(equal(@(UINT16_MAX)));
+ expect(testCell.uniqueText).to(equal(testText));
});
});
diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLChoiceSetManagerSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLChoiceSetManagerSpec.m
index f7379b6f0..3fde3aa15 100644
--- a/SmartDeviceLinkTests/DevAPISpecs/SDLChoiceSetManagerSpec.m
+++ b/SmartDeviceLinkTests/DevAPISpecs/SDLChoiceSetManagerSpec.m
@@ -65,6 +65,7 @@
- (void)sdl_hmiStatusNotification:(SDLRPCNotificationNotification *)notification;
- (void)sdl_displayCapabilityDidUpdate;
+- (void)sdl_addUniqueNamesToCells:(NSMutableSet<SDLChoiceCell *> *)choices;
@end
@@ -84,6 +85,9 @@ describe(@"choice set manager tests", ^{
__block SDLChoiceCell *testCell1 = nil;
__block SDLChoiceCell *testCell2 = nil;
__block SDLChoiceCell *testCell3 = nil;
+ __block SDLChoiceCell *testCellDuplicate = nil;
+ __block SDLVersion *choiceSetUniquenessActiveVersion = nil;
+ __block SDLArtwork *testArtwork = nil;
beforeEach(^{
testConnectionManager = [[TestConnectionManager alloc] init];
@@ -92,9 +96,11 @@ describe(@"choice set manager tests", ^{
testManager = [[SDLChoiceSetManager alloc] initWithConnectionManager:testConnectionManager fileManager:testFileManager systemCapabilityManager:testSystemCapabilityManager];
+ testArtwork = [[SDLArtwork alloc] initWithStaticIcon:SDLStaticIconNameKey];
testCell1 = [[SDLChoiceCell alloc] initWithText:@"test1"];
testCell2 = [[SDLChoiceCell alloc] initWithText:@"test2"];
testCell3 = [[SDLChoiceCell alloc] initWithText:@"test3"];
+ testCellDuplicate = [[SDLChoiceCell alloc] initWithText:@"test1" artwork:nil voiceCommands:nil];
enabledWindowCapability = [[SDLWindowCapability alloc] init];
enabledWindowCapability.textFields = @[[[SDLTextField alloc] initWithName:SDLTextFieldNameMenuName characterSet:SDLCharacterSetUtf8 width:500 rows:1]];
@@ -102,6 +108,7 @@ describe(@"choice set manager tests", ^{
disabledWindowCapability.textFields = @[];
blankWindowCapability = [[SDLWindowCapability alloc] init];
blankWindowCapability.textFields = @[];
+ choiceSetUniquenessActiveVersion = [[SDLVersion alloc] initWithMajor:7 minor:1 patch:0];
});
it(@"should be in the correct startup state", ^{
@@ -256,6 +263,46 @@ describe(@"choice set manager tests", ^{
});
});
+ context(@"when some choices are already uploaded with duplicate titles version >= 7.1.0", ^{
+ beforeEach(^{
+ [SDLGlobals sharedGlobals].rpcVersion = choiceSetUniquenessActiveVersion;
+ [testManager preloadChoices:@[testCell1, testCellDuplicate] withCompletionHandler:^(NSError * _Nullable error) { }];
+ });
+
+ it(@"should not update the choiceCells' unique title", ^{
+ SDLPreloadChoicesOperation *testOp = testManager.transactionQueue.operations.firstObject;
+ testOp.completionBlock();
+ NSArray <SDLChoiceCell *> *testArrays = testManager.preloadedChoices.allObjects;
+ for (SDLChoiceCell *choiceCell in testArrays) {
+ expect(choiceCell.uniqueText).to(equal("test1"));
+ }
+ expect(testManager.preloadedChoices).to(contain(testCell1));
+ expect(testManager.preloadedChoices).to(contain(testCellDuplicate));
+ });
+ });
+
+ context(@"when some choices are already uploaded with duplicate titles version <= 7.1.0", ^{
+ beforeEach(^{
+ [SDLGlobals sharedGlobals].rpcVersion = [[SDLVersion alloc] initWithMajor:7 minor:0 patch:0];
+ [testManager preloadChoices:@[testCell1, testCellDuplicate] withCompletionHandler:^(NSError * _Nullable error) { }];
+ });
+
+ it(@"append a number to the unique text for choice set cells", ^{
+ SDLPreloadChoicesOperation *testOp = testManager.transactionQueue.operations.firstObject;
+ testOp.completionBlock();
+ NSArray <SDLChoiceCell *> *testArrays = testManager.preloadedChoices.allObjects;
+ for (SDLChoiceCell *choiceCell in testArrays) {
+ if (choiceCell.artwork) {
+ expect(choiceCell.uniqueText).to(equal("test1 (2)"));
+ } else {
+ expect(choiceCell.uniqueText).to(equal("test1"));
+ }
+ }
+ expect(testManager.preloadedChoices).to(contain(testCell1));
+ expect(testManager.preloadedChoices).to(contain(testCellDuplicate));
+ });
+ });
+
context(@"when some choices are already pending", ^{
beforeEach(^{
testManager.pendingMutablePreloadChoices = [NSMutableSet setWithArray:@[testCell1]];
diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLChoiceSetSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLChoiceSetSpec.m
index 76d1e4338..63a8a6821 100644
--- a/SmartDeviceLinkTests/DevAPISpecs/SDLChoiceSetSpec.m
+++ b/SmartDeviceLinkTests/DevAPISpecs/SDLChoiceSetSpec.m
@@ -8,6 +8,7 @@
#import "SDLChoiceSetDelegate.h"
#import "SDLTTSChunk.h"
#import "SDLVrHelpItem.h"
+#import "SDLArtwork.h"
@interface SDLChoiceSet()
@@ -156,23 +157,24 @@ describe(@"an SDLChoiceSet", ^{
expect(testChoiceSet).to(beNil());
});
- it(@"should return nil with equivalent cell text", ^{
+ it(@"should return nil when 2 or more cells are identical", ^{
+ // Cells cannot be identical
+ SDLArtwork *testArtwork = [[SDLArtwork alloc] initWithStaticIcon:SDLStaticIconNameKey];
+ SDLChoiceCell *equalCell = [[SDLChoiceCell alloc] initWithText:@"Text" secondaryText:@"Text 2" tertiaryText:nil voiceCommands:nil artwork:nil secondaryArtwork:testArtwork];
+ SDLChoiceCell *equalCell2 = [[SDLChoiceCell alloc] initWithText:@"Text" secondaryText:@"Text 2" tertiaryText:nil voiceCommands:nil artwork:nil secondaryArtwork:testArtwork];
+ testChoiceSet = [[SDLChoiceSet alloc] initWithTitle:testTitle delegate:testDelegate choices:@[equalCell, equalCell2]];
+ expect(testChoiceSet).to(beNil());
+ });
+
+ it(@"should return nil when 2 or more cells voice commands are identical", ^{
// Cell `text` cannot be equal
- SDLChoiceCell *equalCell = [[SDLChoiceCell alloc] initWithText:@"Text"];
- SDLChoiceCell *equalCell2 = [[SDLChoiceCell alloc] initWithText:@"Text"];
+ SDLChoiceCell *equalCell = [[SDLChoiceCell alloc] initWithText:@"Text" artwork:nil voiceCommands:@[@"Kit", @"Kat"]];
+ SDLChoiceCell *equalCell2 = [[SDLChoiceCell alloc] initWithText:@"Text 2" artwork:nil voiceCommands:@[@"Kat"]];
testChoiceSet = [[SDLChoiceSet alloc] initWithTitle:testTitle delegate:testDelegate choices:@[equalCell, equalCell2]];
expect(testChoiceSet).to(beNil());
});
context(@"With bad VR data", ^{
- it(@"should return nil if not all choice set items have voice commands", ^{
- // Cell `voiceCommands` cannot be equal
- SDLChoiceCell *equalCellVR = [[SDLChoiceCell alloc] initWithText:@"Text" artwork:nil voiceCommands:@[@"vr"]];
- SDLChoiceCell *equalCellVR2 = [[SDLChoiceCell alloc] initWithText:@"Text2" artwork:nil voiceCommands:nil];
- testChoiceSet = [[SDLChoiceSet alloc] initWithTitle:testTitle delegate:testDelegate choices:@[equalCellVR, equalCellVR2]];
- expect(testChoiceSet).to(beNil());
- });
-
it(@"should return nil if there are duplicate voice command strings in the choice set", ^{
// Cell `voiceCommands` cannot be equal
SDLChoiceCell *equalCellVR = [[SDLChoiceCell alloc] initWithText:@"Text" artwork:nil voiceCommands:@[@"Dog"]];
diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLFileManagerSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLFileManagerSpec.m
index f7ea93fc8..ef6f00637 100644
--- a/SmartDeviceLinkTests/DevAPISpecs/SDLFileManagerSpec.m
+++ b/SmartDeviceLinkTests/DevAPISpecs/SDLFileManagerSpec.m
@@ -10,6 +10,7 @@
#import "SDLFileManagerConfiguration.h"
#import "SDLFileType.h"
#import "SDLFileWrapper.h"
+#import "SDLGlobals.h"
#import "SDLListFiles.h"
#import "SDLListFilesOperation.h"
#import "SDLListFilesResponse.h"
@@ -320,19 +321,20 @@ describe(@"uploading / deleting single files with the file manager", ^{
});
});
- context(@"when allow overwrite is NO", ^{
+ context(@"when allow overwrite is NO and the RPC version is < 4.4.0", ^{
__block NSString *testUploadFileName = nil;
__block Boolean testUploadOverwrite = NO;
beforeEach(^{
testUploadFileName = [testInitialFileNames lastObject];
+ [SDLGlobals sharedGlobals].rpcVersion = [SDLVersion versionWithMajor:4 minor:3 patch:0];
});
- it(@"should not upload the file if persistance is YES", ^{
- SDLFile *persistantFile = [[SDLFile alloc] initWithData:testFileData name:testUploadFileName fileExtension:@"bin" persistent:YES];
- persistantFile.overwrite = testUploadOverwrite;
+ it(@"should not upload the file if persistence is YES", ^{
+ SDLFile *persistentFile = [[SDLFile alloc] initWithData:testFileData name:testUploadFileName fileExtension:@"bin" persistent:YES];
+ persistentFile.overwrite = testUploadOverwrite;
- [testFileManager uploadFile:persistantFile completionHandler:^(BOOL success, NSUInteger bytesAvailable, NSError * _Nullable error) {
+ [testFileManager uploadFile:persistentFile completionHandler:^(BOOL success, NSUInteger bytesAvailable, NSError * _Nullable error) {
expect(@(success)).to(beFalse());
expect(@(bytesAvailable)).to(equal(@(testFileManager.bytesAvailable)));
expect(error).to(equal([NSError sdl_fileManager_cannotOverwriteError]));
@@ -341,11 +343,11 @@ describe(@"uploading / deleting single files with the file manager", ^{
expect(testFileManager.pendingTransactions.count).to(equal(0));
});
- it(@"should upload the file if persistance is NO", ^{
- SDLFile *unPersistantFile = [[SDLFile alloc] initWithData:testFileData name:testUploadFileName fileExtension:@"bin" persistent:NO];
- unPersistantFile.overwrite = testUploadOverwrite;
+ it(@"should upload the file if persistence is NO", ^{
+ SDLFile *unPersistentFile = [[SDLFile alloc] initWithData:testFileData name:testUploadFileName fileExtension:@"bin" persistent:NO];
+ unPersistentFile.overwrite = testUploadOverwrite;
- [testFileManager uploadFile:unPersistantFile completionHandler:^(BOOL success, NSUInteger bytesAvailable, NSError * _Nullable error) {
+ [testFileManager uploadFile:unPersistentFile completionHandler:^(BOOL success, NSUInteger bytesAvailable, NSError * _Nullable error) {
expect(success).to(beTrue());
expect(bytesAvailable).to(equal(newBytesAvailable));
expect(error).to(beNil());
@@ -473,6 +475,10 @@ describe(@"uploading / deleting single files with the file manager", ^{
__block SDLArtwork *artwork = nil;
context(@"when artwork is nil", ^{
+ beforeEach(^{
+ artwork = nil;
+ });
+
it(@"should not allow file to be uploaded", ^{
expect(artwork).to(beNil());
BOOL testFileNeedsUpload = [testFileManager fileNeedsUpload:artwork];
diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLMenuCellSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuCellSpec.m
index 33381055e..fd4349887 100644
--- a/SmartDeviceLinkTests/DevAPISpecs/SDLMenuCellSpec.m
+++ b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuCellSpec.m
@@ -60,6 +60,7 @@ describe(@"a menu cell", ^{
expect(testCell.icon).to(equal(someArtwork));
expect(testCell.voiceCommands).to(equal(someVoiceCommands));
expect(testCell.subCells).to(beNil());
+ expect(testCell.uniqueTitle).to(equal(someTitle));
expect(testCell.secondaryText).to(beNil());
expect(testCell.tertiaryText).to(beNil());
expect(testCell.secondaryArtwork).to(beNil());
@@ -85,6 +86,7 @@ describe(@"a menu cell", ^{
expect(testCell.voiceCommands).to(beNil());
expect(testCell.subCells).to(equal(someSubcells));
expect(testCell.submenuLayout).to(equal(testLayout));
+ expect(testCell.uniqueTitle).to(equal(someTitle));
expect(testCell.secondaryText).to(equal(someSecondaryTitle));
expect(testCell.tertiaryText).to(equal(someTertiaryTitle));
expect(testCell.secondaryArtwork).to(equal(someSecondaryArtwork));
diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLMenuManagerSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuManagerSpec.m
index 816091d98..c14e177a6 100644
--- a/SmartDeviceLinkTests/DevAPISpecs/SDLMenuManagerSpec.m
+++ b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuManagerSpec.m
@@ -53,11 +53,15 @@ describe(@"menu manager", ^{
__block SDLMenuCell *textOnlyCell = nil;
__block SDLMenuCell *textOnlyCell2 = nil;
__block SDLMenuCell *textAndImageCell = nil;
+ __block SDLMenuCell *textAndImageCell2 = nil;
__block SDLMenuCell *submenuCell = nil;
+ __block SDLMenuCell *submenuCell2 = nil;
__block SDLMenuCell *submenuImageCell = nil;
__block SDLMenuConfiguration *testMenuConfiguration = nil;
+ __block SDLVersion *menuUniquenessActiveVersion = nil;
+
beforeEach(^{
testArtwork = [[SDLArtwork alloc] initWithData:[@"Test data" dataUsingEncoding:NSUTF8StringEncoding] name:@"some artwork name" fileExtension:@"png" persistent:NO];
testArtwork2 = [[SDLArtwork alloc] initWithData:[@"Test data 2" dataUsingEncoding:NSUTF8StringEncoding] name:@"some artwork name 2" fileExtension:@"png" persistent:NO];
@@ -66,7 +70,9 @@ describe(@"menu manager", ^{
textOnlyCell = [[SDLMenuCell alloc] initWithTitle:@"Test 1" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}];
textAndImageCell = [[SDLMenuCell alloc] initWithTitle:@"Test 2" secondaryText:nil tertiaryText:nil icon:testArtwork secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}];
+ textAndImageCell2 = [[SDLMenuCell alloc] initWithTitle:@"Test 2" secondaryText:nil tertiaryText:nil icon:testArtwork2 secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}];
submenuCell = [[SDLMenuCell alloc] initWithTitle:@"Test 3" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil submenuLayout:nil subCells:@[textOnlyCell, textAndImageCell]];
+ submenuCell2 = [[SDLMenuCell alloc] initWithTitle:@"Test 3" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil submenuLayout:nil subCells:@[textAndImageCell, textAndImageCell2]];
submenuImageCell = [[SDLMenuCell alloc] initWithTitle:@"Test 4" secondaryText:nil tertiaryText:nil icon:testArtwork2 secondaryArtwork:nil submenuLayout:SDLMenuLayoutTiles subCells:@[textOnlyCell]];
textOnlyCell2 = [[SDLMenuCell alloc] initWithTitle:@"Test 5" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}];
@@ -90,6 +96,7 @@ describe(@"menu manager", ^{
windowCapability.imageTypeSupported = @[SDLImageTypeDynamic, SDLImageTypeStatic];
windowCapability.menuLayoutsAvailable = @[SDLMenuLayoutList, SDLMenuLayoutTiles];
testManager.windowCapability = windowCapability;
+ menuUniquenessActiveVersion = [[SDLVersion alloc] initWithMajor:7 minor:1 patch:0];
});
it(@"should instantiate correctly", ^{
@@ -188,9 +195,63 @@ describe(@"menu manager", ^{
testManager.currentSystemContext = SDLSystemContextMain;
});
- context(@"duplicate titles", ^{
- it(@"should fail with a duplicate title", ^{
- testManager.menuCells = @[textOnlyCell, textOnlyCell];
+ context(@"duplicate titles version >= 7.1.0", ^{
+ beforeEach(^{
+ [SDLGlobals sharedGlobals].rpcVersion = menuUniquenessActiveVersion;
+ });
+
+ it(@"should not update the cells' unique title", ^{
+ testManager.menuCells = @[textAndImageCell, textAndImageCell2];
+ expect(testManager.menuCells).toNot(beEmpty());
+ expect(testManager.menuCells.firstObject.uniqueTitle).to(equal("Test 2"));
+ expect(testManager.menuCells.lastObject.uniqueTitle).to(equal("Test 2"));
+ });
+
+ it(@"should not update subcells' unique title", ^{
+ testManager.menuCells = @[submenuCell2];
+ expect(testManager.menuCells).toNot(beEmpty());
+ expect(testManager.menuCells.firstObject.subCells.firstObject.uniqueTitle).to(equal("Test 2"));
+ expect(testManager.menuCells.firstObject.subCells.lastObject.uniqueTitle).to(equal("Test 2"));
+ });
+ });
+
+ context(@"duplicate titles version <= 7.1.0", ^{
+ beforeEach(^{
+ [SDLGlobals sharedGlobals].rpcVersion = [[SDLVersion alloc] initWithMajor:7 minor:0 patch:0];
+ });
+
+ it(@"append a number to the unique text for main menu cells", ^{
+ testManager.menuCells = @[textAndImageCell, textAndImageCell2];
+ expect(testManager.menuCells).toNot(beEmpty());
+ expect(testManager.menuCells.firstObject.uniqueTitle).to(equal("Test 2"));
+ expect(testManager.menuCells.lastObject.uniqueTitle).to(equal("Test 2 (2)"));
+ });
+
+ it(@"should append a number to the unique text for subcells", ^{
+ testManager.menuCells = @[submenuCell2];
+ expect(testManager.menuCells).toNot(beEmpty());
+ expect(testManager.menuCells.firstObject.subCells.firstObject.uniqueTitle).to(equal("Test 2"));
+ expect(testManager.menuCells.firstObject.subCells.lastObject.uniqueTitle).to(equal("Test 2 (2)"));
+ });
+ });
+
+ context(@"when the cells contain duplicates", ^{
+ SDLMenuCell *textCell = [[SDLMenuCell alloc] initWithTitle:@"Test 1" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:@[@"no", @"yes"] handler:^(SDLTriggerSource _Nonnull triggerSource) {}];
+ SDLMenuCell *textCell2 = [[SDLMenuCell alloc] initWithTitle:@"Test 1" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:@[@"no", @"yes"] handler:^(SDLTriggerSource _Nonnull triggerSource) {}];
+
+ it(@"should fail with duplicate cells", ^{
+ testManager.menuCells = @[textCell, textCell2];
+ expect(testManager.menuCells).to(beEmpty());
+ });
+ });
+
+ context(@"when cells contain duplicate subcells", ^{
+ SDLMenuCell *subCell1 = [[SDLMenuCell alloc] initWithTitle:@"subCell 1" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}];
+ SDLMenuCell *subCell2 = [[SDLMenuCell alloc] initWithTitle:@"subCell 1" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}];
+ SDLMenuCell *textCell = [[SDLMenuCell alloc] initWithTitle:@"Test 1" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil submenuLayout:nil subCells:@[subCell1, subCell2]];
+
+ it(@"should fail with duplicate cells", ^{
+ testManager.menuCells = @[textCell];
expect(testManager.menuCells).to(beEmpty());
});
});
@@ -205,6 +266,18 @@ describe(@"menu manager", ^{
});
});
+ context(@"when there are duplicate VR commands in subCells", ^{
+ SDLMenuCell *textAndVRSubCell1 = [[SDLMenuCell alloc] initWithTitle:@"subCell 1" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:@[@"Cat"] handler:^(SDLTriggerSource _Nonnull triggerSource) {}];
+ SDLMenuCell *textAndVRSubCell2 = [[SDLMenuCell alloc] initWithTitle:@"subCell 2" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}];
+ SDLMenuCell *textAndVRCell1 = [[SDLMenuCell alloc] initWithTitle:@"Test 1" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:@[@"Cat", @"Turtle"] handler:^(SDLTriggerSource _Nonnull triggerSource) {}];
+ SDLMenuCell *textAndVRCell2 = [[SDLMenuCell alloc] initWithTitle:@"Test 2" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil submenuLayout:nil subCells:@[textAndVRSubCell1, textAndVRSubCell2]];
+
+ it(@"should fail when menu items have duplicate vr commands", ^{
+ testManager.menuCells = @[textAndVRCell1, textAndVRCell2];
+ expect(testManager.menuCells).to(beEmpty());
+ });
+ });
+
it(@"should check if all artworks are uploaded and return NO", ^{
textAndImageCell = [[SDLMenuCell alloc] initWithTitle:@"Test 2" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:testArtwork voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}];
testManager.menuCells = @[textAndImageCell, textOnlyCell];
diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLPreloadChoicesOperationSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLPreloadChoicesOperationSpec.m
index 144f54e3d..646189519 100644
--- a/SmartDeviceLinkTests/DevAPISpecs/SDLPreloadChoicesOperationSpec.m
+++ b/SmartDeviceLinkTests/DevAPISpecs/SDLPreloadChoicesOperationSpec.m
@@ -58,8 +58,8 @@ describe(@"a preload choices operation", ^{
});
context(@"with artworks", ^{
- __block NSSet<SDLChoiceCell *> *cellsWithArtwork = nil;
- __block NSSet<SDLChoiceCell *> *cellsWithStaticIcon = nil;
+ __block NSOrderedSet<SDLChoiceCell *> *cellsWithArtwork = nil;
+ __block NSOrderedSet<SDLChoiceCell *> *cellsWithStaticIcon = nil;
__block NSString *art1Name = @"Art1Name";
__block NSString *art2Name = @"Art2Name";
__block SDLArtwork *cell1Art2 = [[SDLArtwork alloc] initWithData:cellArtData2 name:art1Name fileExtension:@"png" persistent:NO];
@@ -74,8 +74,8 @@ describe(@"a preload choices operation", ^{
SDLArtwork *staticIconArt = [SDLArtwork artworkWithStaticIcon:SDLStaticIconNameDate];
SDLChoiceCell *cellWithStaticIcon = [[SDLChoiceCell alloc] initWithText:@"Static Icon" secondaryText:nil tertiaryText:nil voiceCommands:nil artwork:staticIconArt secondaryArtwork:nil];
- cellsWithArtwork = [NSSet setWithArray:@[cell1WithArt, cell2WithArtAndSecondary]];
- cellsWithStaticIcon = [NSSet setWithArray:@[cellWithStaticIcon]];
+ cellsWithArtwork = [[NSOrderedSet alloc] initWithArray:@[cell1WithArt, cell2WithArtAndSecondary]];
+ cellsWithStaticIcon = [[NSOrderedSet alloc] initWithArray:@[cellWithStaticIcon]];
});
context(@"if the menuName is not set", ^{
@@ -159,8 +159,8 @@ describe(@"a preload choices operation", ^{
SDLArtwork *staticIconArt = [SDLArtwork artworkWithStaticIcon:SDLStaticIconNameDate];
SDLChoiceCell *cellWithStaticIcon = [[SDLChoiceCell alloc] initWithText:@"Static Icon" secondaryText:nil tertiaryText:nil voiceCommands:nil artwork:staticIconArt secondaryArtwork:nil];
- cellsWithArtwork = [NSSet setWithArray:@[cell1WithArt, cell2WithArtAndSecondary]];
- cellsWithStaticIcon = [NSSet setWithArray:@[cellWithStaticIcon]];
+ cellsWithArtwork = [[NSOrderedSet alloc] initWithArray:@[cell1WithArt, cell2WithArtAndSecondary]];
+ cellsWithStaticIcon = [[NSOrderedSet alloc] initWithArray:@[cellWithStaticIcon]];
testOp = [[SDLPreloadChoicesOperation alloc] initWithConnectionManager:testConnectionManager fileManager:testFileManager displayName:testDisplayName windowCapability:windowCapability isVROptional:NO cellsToPreload:cellsWithArtwork];
[testOp start];
@@ -200,12 +200,12 @@ describe(@"a preload choices operation", ^{
});
context(@"without artworks", ^{
- __block NSSet<SDLChoiceCell *> *cellsWithoutArtwork = nil;
+ __block NSOrderedSet<SDLChoiceCell *> *cellsWithoutArtwork = nil;
beforeEach(^{
SDLChoiceCell *cellBasic = [[SDLChoiceCell alloc] initWithText:@"Cell1" artwork:nil voiceCommands:nil];
SDLChoiceCell *cellWithVR = [[SDLChoiceCell alloc] initWithText:@"Cell2" secondaryText:nil tertiaryText:nil voiceCommands:@[@"Cell2"] artwork:nil secondaryArtwork:nil];
SDLChoiceCell *cellWithAllText = [[SDLChoiceCell alloc] initWithText:@"Cell2" secondaryText:@"Cell2" tertiaryText:@"Cell2" voiceCommands:nil artwork:nil secondaryArtwork:nil];
- cellsWithoutArtwork = [NSSet setWithArray:@[cellBasic, cellWithVR, cellWithAllText]];
+ cellsWithoutArtwork = [[NSOrderedSet alloc] initWithArray:@[cellBasic, cellWithVR, cellWithAllText]];
});
it(@"should skip to preloading cells", ^{