diff --git a/Cartfile.private b/Cartfile.private index cedbbe1a3..a261ab097 100644 --- a/Cartfile.private +++ b/Cartfile.private @@ -1,3 +1,3 @@ github "Quick/Quick" ~> 3.0 -github "Quick/Nimble" ~> 9.0.0 +github "Quick/Nimble" ~> 9.0 github "erikdoe/ocmock" ~> 3.7 diff --git a/SmartDeviceLink-iOS.xcodeproj/project.pbxproj b/SmartDeviceLink-iOS.xcodeproj/project.pbxproj index 0f3a0641a..4fe6cebb4 100644 --- a/SmartDeviceLink-iOS.xcodeproj/project.pbxproj +++ b/SmartDeviceLink-iOS.xcodeproj/project.pbxproj @@ -513,9 +513,22 @@ 4A8BD3CD24F999BE000945E3 /* TestSubscribeButtonObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A8BD3CC24F999BE000945E3 /* TestSubscribeButtonObserver.m */; }; 4A8BD3D024FE7CF1000945E3 /* SDLPermissionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 4A8BD3CE24FE7CF1000945E3 /* SDLPermissionManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4A8BD3D124FE7CF1000945E3 /* SDLPermissionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A8BD3CF24FE7CF1000945E3 /* SDLPermissionManager.m */; }; + 4A93893D25B8CBD40069F438 /* SDLMenuReplaceOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 4A93893B25B8CBD40069F438 /* SDLMenuReplaceOperation.h */; }; + 4A93893E25B8CBD40069F438 /* SDLMenuReplaceOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A93893C25B8CBD40069F438 /* SDLMenuReplaceOperation.m */; }; + 4A93895325B9DACA0069F438 /* SDLMenuShowOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 4A93895125B9DACA0069F438 /* SDLMenuShowOperation.h */; }; + 4A93895425B9DACA0069F438 /* SDLMenuShowOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A93895225B9DACA0069F438 /* SDLMenuShowOperation.m */; }; + 4A93895925B9E5E40069F438 /* SDLMenuConfigurationUpdateOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 4A93895725B9E5E40069F438 /* SDLMenuConfigurationUpdateOperation.h */; }; + 4A93895A25B9E5E40069F438 /* SDLMenuConfigurationUpdateOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A93895825B9E5E40069F438 /* SDLMenuConfigurationUpdateOperation.m */; }; + 4A93896625BB361C0069F438 /* SDLMenuReplaceUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 4A93896425BB361C0069F438 /* SDLMenuReplaceUtilities.h */; }; + 4A93896725BB361C0069F438 /* SDLMenuReplaceUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A93896525BB361C0069F438 /* SDLMenuReplaceUtilities.m */; }; + 4A96113B25D1A0C600D787DA /* SDLMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 4A96113925D1A0C600D787DA /* SDLMacros.h */; }; 4AAB6A1225E57BEA0017A5A7 /* SDLSystemInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 4AAB6A1025E57BEA0017A5A7 /* SDLSystemInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4AAB6A1325E57BEA0017A5A7 /* SDLSystemInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 4AAB6A1125E57BEA0017A5A7 /* SDLSystemInfo.m */; }; 4AAB6A2325E69D010017A5A7 /* SDLSystemInfoSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4AAB6A2225E69D010017A5A7 /* SDLSystemInfoSpec.m */; }; + 4AAC0DBA25C1FEBA00746D33 /* SDLMenuManagerPrivateConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 4AAC0DB825C1FEBA00746D33 /* SDLMenuManagerPrivateConstants.h */; }; + 4AAC0DBB25C1FEBA00746D33 /* SDLMenuManagerPrivateConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 4AAC0DB925C1FEBA00746D33 /* SDLMenuManagerPrivateConstants.m */; }; + 4AAC0DE025C468EC00746D33 /* SDLMenuReplaceUtilitiesSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4AAC0DDF25C468EC00746D33 /* SDLMenuReplaceUtilitiesSpec.m */; }; + 4AAC0DE825C493EE00746D33 /* SDLMenuReplaceUtilitiesSpecHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 4AAC0DE725C493EE00746D33 /* SDLMenuReplaceUtilitiesSpecHelpers.m */; }; 4ABB24BA24F592620061BF55 /* NSMutableArray+Safe.h in Headers */ = {isa = PBXBuildFile; fileRef = 4ABB24B224F592620061BF55 /* NSMutableArray+Safe.h */; }; 4ABB24BB24F592620061BF55 /* NSMutableArray+Safe.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ABB24B324F592620061BF55 /* NSMutableArray+Safe.m */; }; 4ABB24BC24F592620061BF55 /* NSBundle+SDLBundle.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ABB24B424F592620061BF55 /* NSBundle+SDLBundle.m */; }; @@ -1387,6 +1400,9 @@ 4ABB2BA724F850AE0061BF55 /* SDLImage.h in Headers */ = {isa = PBXBuildFile; fileRef = 4ABB2B9924F850AD0061BF55 /* SDLImage.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4ABB2BA824F850AE0061BF55 /* SDLLightState.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ABB2B9A24F850AD0061BF55 /* SDLLightState.m */; }; 4ABB2BA924F850AE0061BF55 /* SDLImageResolution.h in Headers */ = {isa = PBXBuildFile; fileRef = 4ABB2B9B24F850AD0061BF55 /* SDLImageResolution.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4ABC1CA725DC4E1C00545AC6 /* SDLMenuReplaceOperationSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ABC1CA625DC4E1C00545AC6 /* SDLMenuReplaceOperationSpec.m */; }; + 4ABC1CAD25DC51A800545AC6 /* SDLMenuConfigurationUpdateOperationSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ABC1CAC25DC51A800545AC6 /* SDLMenuConfigurationUpdateOperationSpec.m */; }; + 4ABC1CB125DC520300545AC6 /* SDLMenuShowOperationSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ABC1CB025DC520300545AC6 /* SDLMenuShowOperationSpec.m */; }; 4ABED25B257681ED005BDF61 /* SDLVoiceCommandUpdateOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ABED259257681ED005BDF61 /* SDLVoiceCommandUpdateOperation.m */; }; 4ABED25C257681ED005BDF61 /* SDLVoiceCommandUpdateOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 4ABED25A257681ED005BDF61 /* SDLVoiceCommandUpdateOperation.h */; }; 4AC0128026D9612E00537E31 /* SDLPreloadPresentChoicesOperationUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 4AC0127E26D9612E00537E31 /* SDLPreloadPresentChoicesOperationUtilities.h */; }; @@ -1740,7 +1756,6 @@ C93193DD26B1B57C008203EC /* SDLSecurityQueryPayload.m in Sources */ = {isa = PBXBuildFile; fileRef = C93193DB26B1B57B008203EC /* SDLSecurityQueryPayload.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 */; }; C971E3EE2649BD8700FC24D6 /* SDLOnSystemRequestSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 162E82371A9BDE8A00906325 /* SDLOnSystemRequestSpec.m */; }; C971E3EF2649C52000FC24D6 /* SDLResponseDispatcherSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DBAE0AC1D368D1A00CE00BF /* SDLResponseDispatcherSpec.m */; }; @@ -2382,9 +2397,24 @@ 4A8BD3CC24F999BE000945E3 /* TestSubscribeButtonObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestSubscribeButtonObserver.m; sourceTree = ""; }; 4A8BD3CE24FE7CF1000945E3 /* SDLPermissionManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDLPermissionManager.h; path = public/SDLPermissionManager.h; sourceTree = ""; }; 4A8BD3CF24FE7CF1000945E3 /* SDLPermissionManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SDLPermissionManager.m; path = public/SDLPermissionManager.m; sourceTree = ""; }; + 4A93893B25B8CBD40069F438 /* SDLMenuReplaceOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDLMenuReplaceOperation.h; path = private/SDLMenuReplaceOperation.h; sourceTree = ""; }; + 4A93893C25B8CBD40069F438 /* SDLMenuReplaceOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLMenuReplaceOperation.m; path = private/SDLMenuReplaceOperation.m; sourceTree = ""; }; + 4A93895125B9DACA0069F438 /* SDLMenuShowOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDLMenuShowOperation.h; path = private/SDLMenuShowOperation.h; sourceTree = ""; }; + 4A93895225B9DACA0069F438 /* SDLMenuShowOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLMenuShowOperation.m; path = private/SDLMenuShowOperation.m; sourceTree = ""; }; + 4A93895725B9E5E40069F438 /* SDLMenuConfigurationUpdateOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDLMenuConfigurationUpdateOperation.h; path = private/SDLMenuConfigurationUpdateOperation.h; sourceTree = ""; }; + 4A93895825B9E5E40069F438 /* SDLMenuConfigurationUpdateOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLMenuConfigurationUpdateOperation.m; path = private/SDLMenuConfigurationUpdateOperation.m; sourceTree = ""; }; + 4A93896425BB361C0069F438 /* SDLMenuReplaceUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDLMenuReplaceUtilities.h; path = private/SDLMenuReplaceUtilities.h; sourceTree = ""; }; + 4A93896525BB361C0069F438 /* SDLMenuReplaceUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLMenuReplaceUtilities.m; path = private/SDLMenuReplaceUtilities.m; sourceTree = ""; }; + 4A96113925D1A0C600D787DA /* SDLMacros.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDLMacros.h; path = private/SDLMacros.h; sourceTree = ""; }; + 4A96114725D1B5DA00D787DA /* SDLMacros.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLMacros.m; path = private/SDLMacros.m; sourceTree = ""; }; 4AAB6A1025E57BEA0017A5A7 /* SDLSystemInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDLSystemInfo.h; path = public/SDLSystemInfo.h; sourceTree = ""; }; 4AAB6A1125E57BEA0017A5A7 /* SDLSystemInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLSystemInfo.m; path = public/SDLSystemInfo.m; sourceTree = ""; }; 4AAB6A2225E69D010017A5A7 /* SDLSystemInfoSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLSystemInfoSpec.m; path = DevAPISpecs/SDLSystemInfoSpec.m; sourceTree = ""; }; + 4AAC0DB825C1FEBA00746D33 /* SDLMenuManagerPrivateConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDLMenuManagerPrivateConstants.h; path = private/SDLMenuManagerPrivateConstants.h; sourceTree = ""; }; + 4AAC0DB925C1FEBA00746D33 /* SDLMenuManagerPrivateConstants.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLMenuManagerPrivateConstants.m; path = private/SDLMenuManagerPrivateConstants.m; sourceTree = ""; }; + 4AAC0DDF25C468EC00746D33 /* SDLMenuReplaceUtilitiesSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLMenuReplaceUtilitiesSpec.m; path = DevAPISpecs/SDLMenuReplaceUtilitiesSpec.m; sourceTree = ""; }; + 4AAC0DE625C493EE00746D33 /* SDLMenuReplaceUtilitiesSpecHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDLMenuReplaceUtilitiesSpecHelpers.h; path = DevAPISpecs/SDLMenuReplaceUtilitiesSpecHelpers.h; sourceTree = ""; }; + 4AAC0DE725C493EE00746D33 /* SDLMenuReplaceUtilitiesSpecHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLMenuReplaceUtilitiesSpecHelpers.m; path = DevAPISpecs/SDLMenuReplaceUtilitiesSpecHelpers.m; sourceTree = ""; }; 4ABB24B224F592620061BF55 /* NSMutableArray+Safe.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSMutableArray+Safe.h"; path = "private/NSMutableArray+Safe.h"; sourceTree = ""; }; 4ABB24B324F592620061BF55 /* NSMutableArray+Safe.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSMutableArray+Safe.m"; path = "private/NSMutableArray+Safe.m"; sourceTree = ""; }; 4ABB24B424F592620061BF55 /* NSBundle+SDLBundle.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSBundle+SDLBundle.m"; path = "private/NSBundle+SDLBundle.m"; sourceTree = ""; }; @@ -3258,6 +3288,9 @@ 4ABB2B9924F850AD0061BF55 /* SDLImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDLImage.h; path = public/SDLImage.h; sourceTree = ""; }; 4ABB2B9A24F850AD0061BF55 /* SDLLightState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SDLLightState.m; path = public/SDLLightState.m; sourceTree = ""; }; 4ABB2B9B24F850AD0061BF55 /* SDLImageResolution.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDLImageResolution.h; path = public/SDLImageResolution.h; sourceTree = ""; }; + 4ABC1CA625DC4E1C00545AC6 /* SDLMenuReplaceOperationSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLMenuReplaceOperationSpec.m; path = DevAPISpecs/SDLMenuReplaceOperationSpec.m; sourceTree = ""; }; + 4ABC1CAC25DC51A800545AC6 /* SDLMenuConfigurationUpdateOperationSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLMenuConfigurationUpdateOperationSpec.m; path = DevAPISpecs/SDLMenuConfigurationUpdateOperationSpec.m; sourceTree = ""; }; + 4ABC1CB025DC520300545AC6 /* SDLMenuShowOperationSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLMenuShowOperationSpec.m; path = DevAPISpecs/SDLMenuShowOperationSpec.m; sourceTree = ""; }; 4ABED259257681ED005BDF61 /* SDLVoiceCommandUpdateOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SDLVoiceCommandUpdateOperation.m; path = private/SDLVoiceCommandUpdateOperation.m; sourceTree = ""; }; 4ABED25A257681ED005BDF61 /* SDLVoiceCommandUpdateOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDLVoiceCommandUpdateOperation.h; path = private/SDLVoiceCommandUpdateOperation.h; sourceTree = ""; }; 4AC0127E26D9612E00537E31 /* SDLPreloadPresentChoicesOperationUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDLPreloadPresentChoicesOperationUtilities.h; path = private/SDLPreloadPresentChoicesOperationUtilities.h; sourceTree = ""; }; @@ -4311,7 +4344,7 @@ path = MessageSpecs; sourceTree = ""; }; - 4A32B3E425559D93001FFA26 /* Voice Cammands */ = { + 4A32B3E425559D93001FFA26 /* Voice Commands */ = { isa = PBXGroup; children = ( 4A32B3E525559DA4001FFA26 /* Cells */, @@ -4319,7 +4352,7 @@ 4ABB25A824F7E6E10061BF55 /* SDLVoiceCommandManager.h */, 4ABB25A724F7E6E10061BF55 /* SDLVoiceCommandManager.m */, ); - name = "Voice Cammands"; + name = "Voice Commands"; sourceTree = ""; }; 4A32B3E525559DA4001FFA26 /* Cells */ = { @@ -4340,18 +4373,6 @@ name = Operations; sourceTree = ""; }; - 4A32B3F325559F37001FFA26 /* Menu */ = { - isa = PBXGroup; - children = ( - 5D76751022D907F500E8D71A /* Configuration */, - 755F175E229F14F70041B9CB /* Dynamic Menu Update Utilities */, - 5D339CEC207C08AB000CC364 /* Cells */, - 4ABB25A924F7E6E10061BF55 /* SDLMenuManager.h */, - 4ABB25A624F7E6E10061BF55 /* SDLMenuManager.m */, - ); - name = Menu; - sourceTree = ""; - }; 4A3BA4D9248E8EBB003E56B8 /* SystemRequest Handler */ = { isa = PBXGroup; children = ( @@ -4426,6 +4447,33 @@ path = Utilities; sourceTree = ""; }; + 4A93893A25B8CB480069F438 /* Operations */ = { + isa = PBXGroup; + children = ( + 4A93896325BB35FA0069F438 /* Menu Replace Utilities */, + 4A93895725B9E5E40069F438 /* SDLMenuConfigurationUpdateOperation.h */, + 4A93895825B9E5E40069F438 /* SDLMenuConfigurationUpdateOperation.m */, + 4A93893B25B8CBD40069F438 /* SDLMenuReplaceOperation.h */, + 4A93893C25B8CBD40069F438 /* SDLMenuReplaceOperation.m */, + 4A93895125B9DACA0069F438 /* SDLMenuShowOperation.h */, + 4A93895225B9DACA0069F438 /* SDLMenuShowOperation.m */, + ); + name = Operations; + sourceTree = ""; + }; + 4A93896325BB35FA0069F438 /* Menu Replace Utilities */ = { + isa = PBXGroup; + children = ( + 4ABB25A124F7E6CE0061BF55 /* SDLDynamicMenuUpdateAlgorithm.h */, + 4ABB259F24F7E6CE0061BF55 /* SDLDynamicMenuUpdateAlgorithm.m */, + 4ABB259E24F7E6CE0061BF55 /* SDLDynamicMenuUpdateRunScore.h */, + 4ABB25A024F7E6CE0061BF55 /* SDLDynamicMenuUpdateRunScore.m */, + 4A93896425BB361C0069F438 /* SDLMenuReplaceUtilities.h */, + 4A93896525BB361C0069F438 /* SDLMenuReplaceUtilities.m */, + ); + name = "Menu Replace Utilities"; + sourceTree = ""; + }; 4A9D02BB2497EEB400FBE99B /* Custom RPC Adapters */ = { isa = PBXGroup; children = ( @@ -4454,6 +4502,35 @@ name = Utilities; sourceTree = ""; }; + 4AAC0DB725C1FE9700746D33 /* Constants */ = { + isa = PBXGroup; + children = ( + 4ABB259824F7E6A60061BF55 /* SDLMenuManagerConstants.h */, + 4AAC0DB825C1FEBA00746D33 /* SDLMenuManagerPrivateConstants.h */, + 4AAC0DB925C1FEBA00746D33 /* SDLMenuManagerPrivateConstants.m */, + ); + name = Constants; + sourceTree = ""; + }; + 4AAC0DE525C493D800746D33 /* Helpers */ = { + isa = PBXGroup; + children = ( + 4AAC0DE625C493EE00746D33 /* SDLMenuReplaceUtilitiesSpecHelpers.h */, + 4AAC0DE725C493EE00746D33 /* SDLMenuReplaceUtilitiesSpecHelpers.m */, + ); + name = Helpers; + sourceTree = ""; + }; + 4ABC1CA525DC4DC200545AC6 /* Operations */ = { + isa = PBXGroup; + children = ( + 4ABC1CAC25DC51A800545AC6 /* SDLMenuConfigurationUpdateOperationSpec.m */, + 4ABC1CA625DC4E1C00545AC6 /* SDLMenuReplaceOperationSpec.m */, + 4ABC1CB025DC520300545AC6 /* SDLMenuShowOperationSpec.m */, + ); + name = Operations; + sourceTree = ""; + }; 4AC0127B26D960E900537E31 /* Utilities */ = { isa = PBXGroup; children = ( @@ -4519,6 +4596,7 @@ 5D0A737F203F23D10001595D /* Soft Button */, 88A098AB2476F08F00A50005 /* Subscribe Button */, 5D0A737D203F23B30001595D /* Text and Graphic */, + 4A32B3E425559D93001FFA26 /* Voice Commands */, 5D1BF6AA2047429C00D36881 /* Utilities */, 4ABB25DA24F7E77C0061BF55 /* SDLScreenManager.h */, 4ABB25DB24F7E77C0061BF55 /* SDLScreenManager.m */, @@ -4650,8 +4728,12 @@ 5D339CE5207C0651000CC364 /* Menu */ = { isa = PBXGroup; children = ( - 4A32B3F325559F37001FFA26 /* Menu */, - 4A32B3E425559D93001FFA26 /* Voice Cammands */, + 4AAC0DB725C1FE9700746D33 /* Constants */, + 5D339CEC207C08AB000CC364 /* Cells */, + 5D76751022D907F500E8D71A /* Configuration */, + 4A93893A25B8CB480069F438 /* Operations */, + 4ABB25A924F7E6E10061BF55 /* SDLMenuManager.h */, + 4ABB25A624F7E6E10061BF55 /* SDLMenuManager.m */, ); name = Menu; sourceTree = ""; @@ -6008,7 +6090,6 @@ children = ( 4ABB259B24F7E6B90061BF55 /* SDLMenuConfiguration.h */, 4ABB259A24F7E6B90061BF55 /* SDLMenuConfiguration.m */, - 4ABB259824F7E6A60061BF55 /* SDLMenuManagerConstants.h */, ); name = Configuration; sourceTree = ""; @@ -6264,6 +6345,8 @@ 5DA3F35C1BC4484B0026F2D0 /* Notifications */, 5D6008871BE3ED470094A505 /* State Machine */, 4ABB24D824F594190061BF55 /* SDLConnectionManagerType.h */, + 4A96113925D1A0C600D787DA /* SDLMacros.h */, + 4A96114725D1B5DA00D787DA /* SDLMacros.m */, 4ABB24C824F593090061BF55 /* SDLStreamingProtocolDelegate.h */, 4ABB24D524F593ED0061BF55 /* SDLVersion.h */, 4ABB24D424F593EC0061BF55 /* SDLVersion.m */, @@ -6751,26 +6834,18 @@ 5DF40B24208FA7C500DD6FDA /* Menu */ = { isa = PBXGroup; children = ( - 5DF40B25208FA7DE00DD6FDA /* SDLMenuManagerSpec.m */, + 4ABC1CA525DC4DC200545AC6 /* Operations */, + 4AAC0DE525C493D800746D33 /* Helpers */, 5DAB5F502098994C00A020C8 /* SDLMenuCellSpec.m */, + 5D76751522D920FD00E8D71A /* SDLMenuConfigurationSpec.m */, + 5DF40B25208FA7DE00DD6FDA /* SDLMenuManagerSpec.m */, + 4AAC0DDF25C468EC00746D33 /* SDLMenuReplaceUtilitiesSpec.m */, 752ECDB8228C42E100D945F4 /* SDLMenuRunScoreSpec.m */, 752ECDBA228C532600D945F4 /* SDLMenuUpdateAlgorithmSpec.m */, - 5D76751522D920FD00E8D71A /* SDLMenuConfigurationSpec.m */, ); name = Menu; sourceTree = ""; }; - 755F175E229F14F70041B9CB /* Dynamic Menu Update Utilities */ = { - isa = PBXGroup; - children = ( - 4ABB25A124F7E6CE0061BF55 /* SDLDynamicMenuUpdateAlgorithm.h */, - 4ABB259F24F7E6CE0061BF55 /* SDLDynamicMenuUpdateAlgorithm.m */, - 4ABB259E24F7E6CE0061BF55 /* SDLDynamicMenuUpdateRunScore.h */, - 4ABB25A024F7E6CE0061BF55 /* SDLDynamicMenuUpdateRunScore.m */, - ); - name = "Dynamic Menu Update Utilities"; - sourceTree = ""; - }; 880245A120F79BDA00ED195B /* Configuration */ = { isa = PBXGroup; children = ( @@ -7309,11 +7384,13 @@ 88D79EED255D8D5B005FACB1 /* SDLPresentAlertOperation.h in Headers */, 4ABB27E424F800CA0061BF55 /* SDLPrerecordedSpeech.h in Headers */, 4ABB2A2E24F847980061BF55 /* SDLDeleteCommandResponse.h in Headers */, + 4A93895325B9DACA0069F438 /* SDLMenuShowOperation.h in Headers */, 4ABB277524F7FE910061BF55 /* SDLIgnitionStatus.h in Headers */, 4A8BD27924F9343F000945E3 /* SDLPermissionItem.h in Headers */, 4ABB2B5024F84EF50061BF55 /* SDLClusterModeStatus.h in Headers */, 4ABB26CA24F7FAAF0061BF55 /* SDLEnum.h in Headers */, 4ABB251E24F7E3EC0061BF55 /* SDLLifecycleMobileHMIStateHandler.h in Headers */, + 4A96113B25D1A0C600D787DA /* SDLMacros.h in Headers */, 4ABB260724F7E9650061BF55 /* SDLStreamingMediaManagerDataSource.h in Headers */, 4ABB265224F7F58D0061BF55 /* SDLRPCRequestNotification.h in Headers */, 4ABB2A6424F847BB0061BF55 /* SDLListFilesResponse.h in Headers */, @@ -7354,6 +7431,7 @@ 4ABB28ED24F82A6A0061BF55 /* SDLOnKeyboardInput.h in Headers */, B360F9DE255F52A50027CA17 /* SDLSeatStatus.h in Headers */, 5D9FDA991F2A7D3F00A495C8 /* emhashmap.h in Headers */, + 4A93893D25B8CBD40069F438 /* SDLMenuReplaceOperation.h in Headers */, 4ABB255F24F7E59E0061BF55 /* SDLPermissionConstants.h in Headers */, 4ABB270324F7FB8F0061BF55 /* SDLButtonName.h in Headers */, 881BBF50255AC27000761B7E /* SDLAlertView.h in Headers */, @@ -7417,6 +7495,7 @@ 4ABB258124F7E61E0061BF55 /* SDLChoiceCell.h in Headers */, 4ABB270124F7FB8F0061BF55 /* SDLBitsPerSample.h in Headers */, 4ABB253224F7E43A0061BF55 /* SDLAsynchronousRPCOperation.h in Headers */, + 4A93895925B9E5E40069F438 /* SDLMenuConfigurationUpdateOperation.h in Headers */, 4ABB25CC24F7E74F0061BF55 /* SDLTextAndGraphicManager.h in Headers */, 4A8BD3B824F98F64000945E3 /* SDLOnUpdateSubMenu.h in Headers */, 4ABB291324F842160061BF55 /* SDLCreateInteractionChoiceSet.h in Headers */, @@ -7451,6 +7530,7 @@ 4ABB2AF424F849CF0061BF55 /* SDLGenericResponse.h in Headers */, 4ABB280A24F824600061BF55 /* SDLServiceUpdateReason.h in Headers */, 4ABB2B5124F84EF50061BF55 /* SDLDisplayCapability.h in Headers */, + 4A93896625BB361C0069F438 /* SDLMenuReplaceUtilities.h in Headers */, 4ABB2ABD24F847FC0061BF55 /* SDLSliderResponse.h in Headers */, 4ABB28D924F82A6A0061BF55 /* SDLOnSyncPData.h in Headers */, 4A8BD2B924F935BC000945E3 /* SDLSoftButton.h in Headers */, @@ -7573,7 +7653,6 @@ 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 */, @@ -7719,6 +7798,7 @@ 4ABB250524F596450061BF55 /* SDLListFilesOperation.h in Headers */, 4ABB268F24F7F8FC0061BF55 /* SDLHexUtility.h in Headers */, 4A8BD28124F9343F000945E3 /* SDLRemoteControlCapabilities.h in Headers */, + 4AAC0DBA25C1FEBA00746D33 /* SDLMenuManagerPrivateConstants.h in Headers */, 4ABB26DD24F7FAFD0061BF55 /* SDLRPCResponse.h in Headers */, 4ABB2AAE24F847F40061BF55 /* SDLSendLocationResponse.h in Headers */, 4A8BD36B24F94636000945E3 /* SDLProtocolMessageDisassembler.h in Headers */, @@ -7939,7 +8019,7 @@ attributes = { CLASSPREFIX = SDL; LastSwiftUpdateCheck = 0710; - LastUpgradeCheck = 1250; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = smartdevicelink; TargetAttributes = { 5D4019AE1A76EC350006B0C2 = { @@ -8124,6 +8204,7 @@ 4ABB250924F596920061BF55 /* SDLFileWrapper.m in Sources */, 4ABB24E324F5948D0061BF55 /* SDLEncryptionConfiguration.m in Sources */, 4ABB273D24F7FD1D0061BF55 /* SDLDimension.m in Sources */, + 4AAC0DBB25C1FEBA00746D33 /* SDLMenuManagerPrivateConstants.m in Sources */, 4ABB267B24F7F6840061BF55 /* SDLIconArchiveFile.m in Sources */, 4ABB2A2224F847980061BF55 /* SDLChangeRegistrationResponse.m in Sources */, 4A8BD3A524F9485B000945E3 /* SDLVideoStreamingState.m in Sources */, @@ -8238,8 +8319,10 @@ 4ABB274F24F7FD9C0061BF55 /* SDLECallConfirmationStatus.m in Sources */, 4ABB27AF24F7FFDA0061BF55 /* SDLMaintenanceModeStatus.m in Sources */, 4ABB292D24F842A00061BF55 /* SDLDeleteWindow.m in Sources */, + 4A93896725BB361C0069F438 /* SDLMenuReplaceUtilities.m in Sources */, 4ABB299A24F845440061BF55 /* SDLSendHapticData.m in Sources */, 4ABB29DB24F846880061BF55 /* SDLUnregisterAppInterface.m in Sources */, + 4A93895425B9DACA0069F438 /* SDLMenuShowOperation.m in Sources */, 4ABB270824F7FB8F0061BF55 /* SDLAppHMIType.m in Sources */, 4ABB282A24F824E70061BF55 /* SDLSystemAction.m in Sources */, 4ABB297824F844D30061BF55 /* SDLPublishAppService.m in Sources */, @@ -8443,6 +8526,7 @@ 4ABB28C924F82A6A0061BF55 /* SDLOnRCStatus.m in Sources */, 4ABB253124F7E43A0061BF55 /* SDLAsynchronousRPCOperation.m in Sources */, 4ABB2A7524F847D40061BF55 /* SDLPublishAppServiceResponse.m in Sources */, + 4A93893E25B8CBD40069F438 /* SDLMenuReplaceOperation.m in Sources */, 4A8BD3AA24F948CF000945E3 /* SDLDeleteFileOperation.m in Sources */, 4ABB2B4524F84EF50061BF55 /* SDLBodyInformation.m in Sources */, 4ABB25BB24F7E70E0061BF55 /* SDLSoftButtonObject.m in Sources */, @@ -8477,6 +8561,7 @@ 4ABB271524F7FC4E0061BF55 /* SDLCompassDirection.m in Sources */, C99BE00A26C53E7F00DB0B54 /* SDLSecurityQueryErrorCode.m in Sources */, 4ABB254424F7E48D0061BF55 /* SDLLockScreenRootViewController.m in Sources */, + 4A93895A25B9E5E40069F438 /* SDLMenuConfigurationUpdateOperation.m in Sources */, 4ABB265F24F7F5F20061BF55 /* SDLNotificationDispatcher.m in Sources */, 4A8BD31624F938D6000945E3 /* SDLWindowTypeCapabilities.m in Sources */, 4ABB280924F824600061BF55 /* SDLSoftButtonType.m in Sources */, @@ -8730,6 +8815,7 @@ 5DBEFA581F436132009EE295 /* SDLFakeSecurityManager.m in Sources */, 9FA0D00022DF06A0009CF344 /* SDLWindowCapabilitySpec.m in Sources */, 162E82D91A9BDE8A00906325 /* SDLDisplayTypeSpec.m in Sources */, + 4ABC1CB125DC520300545AC6 /* SDLMenuShowOperationSpec.m in Sources */, 00EADD3522DFE5670088B608 /* SDLEncryptionConfigurationSpec.m in Sources */, 162E83871A9BDE8B00906325 /* SDLPermissionItemSpec.m in Sources */, 5DAB5F5320989A8300A020C8 /* SDLVoiceCommandSpec.m in Sources */, @@ -8832,6 +8918,7 @@ 162E83811A9BDE8B00906325 /* SDLImageFieldSpec.m in Sources */, 5D60DF24202B7A80001EDA01 /* SDLAsynchronousRPCRequestOperationSpec.m in Sources */, 883C22CB222EEF0900939C4C /* SDLRPCFunctionNamesSpec.m in Sources */, + 4AAC0DE825C493EE00746D33 /* SDLMenuReplaceUtilitiesSpecHelpers.m in Sources */, 162E834F1A9BDE8B00906325 /* SDLDeleteCommandResponseSpec.m in Sources */, 88B848C91F462E3600DED768 /* TestFileProgressResponse.m in Sources */, 162E83231A9BDE8B00906325 /* SDLAddSubMenuSpec.m in Sources */, @@ -9019,6 +9106,7 @@ 162E82F91A9BDE8B00906325 /* SDLSamplingRateSpec.m in Sources */, 5DBEFA541F434B9E009EE295 /* SDLStreamingMediaConfigurationSpec.m in Sources */, 1EB59CD2202DCA9B00343A61 /* SDLMassageModeSpec.m in Sources */, + 4AAC0DE025C468EC00746D33 /* SDLMenuReplaceUtilitiesSpec.m in Sources */, 9FA0D00C22DF0B65009CF344 /* SDLCreateWindowResponseSpec.m in Sources */, 162E82CB1A9BDE8A00906325 /* SDLAppHMITypeSpec.m in Sources */, EEB2537E2067D3E80069584E /* SDLSecondaryTransportManagerSpec.m in Sources */, @@ -9089,6 +9177,7 @@ 162E836B1A9BDE8B00906325 /* SDLSyncPDataResponseSpec.m in Sources */, 8B7B31AF1F2FBA0200BDC38D /* SDLVideoStreamingCapabilitySpec.m in Sources */, 88FBF7C2250132C1005EA0A4 /* SDLOnLockScreenStatusSpec.m in Sources */, + 4ABC1CAD25DC51A800545AC6 /* SDLMenuConfigurationUpdateOperationSpec.m in Sources */, 8B05F88922DD011300666CD8 /* SDLUnpublishAppServiceSpec.m in Sources */, 162E839B1A9BDE8B00906325 /* SDLRPCNotificationSpec.m in Sources */, 162E83581A9BDE8B00906325 /* SDLGetVehicleDataResponseSpec.m in Sources */, @@ -9177,6 +9266,7 @@ 162E83731A9BDE8B00906325 /* SDLBeltStatusSpec.m in Sources */, 162E83551A9BDE8B00906325 /* SDLEndAudioPassThruResponseSpec.m in Sources */, 8881AFC12225EB9300EA870B /* SDLGetCloudAppPropertiesResponseSpec.m in Sources */, + 4ABC1CA725DC4E1C00545AC6 /* SDLMenuReplaceOperationSpec.m in Sources */, 000DD56E22EF01FC005AB7A7 /* SDLSeatLocationSpec.m in Sources */, 162E83251A9BDE8B00906325 /* SDLAlertSpec.m in Sources */, 2BF2F85220ED068200A26EF2 /* SDLAudioStreamingIndicatorSpec.m in Sources */, @@ -9426,6 +9516,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_COMPLETION_HANDLER_MISUSE = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = NCVC2MHU7M; @@ -9444,6 +9535,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_COMPLETION_HANDLER_MISUSE = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = NCVC2MHU7M; @@ -9469,6 +9561,7 @@ CLANG_STATIC_ANALYZER_MODE = deep; CLANG_UNDEFINED_BEHAVIOR_SANITIZER_INTEGER = YES; CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES; + CLANG_WARN_COMPLETION_HANDLER_MISUSE = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_FLOAT_CONVERSION = YES; CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES; @@ -9525,6 +9618,7 @@ CLANG_STATIC_ANALYZER_MODE = deep; CLANG_UNDEFINED_BEHAVIOR_SANITIZER_INTEGER = YES; CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES; + CLANG_WARN_COMPLETION_HANDLER_MISUSE = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_FLOAT_CONVERSION = YES; CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES; @@ -9642,6 +9736,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CLANG_WARN_COMPLETION_HANDLER_MISUSE = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = NCVC2MHU7M; @@ -9664,6 +9759,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CLANG_WARN_COMPLETION_HANDLER_MISUSE = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = NCVC2MHU7M; diff --git a/SmartDeviceLink-iOS.xcodeproj/xcshareddata/xcschemes/SmartDeviceLink-Example-ObjC.xcscheme b/SmartDeviceLink-iOS.xcodeproj/xcshareddata/xcschemes/SmartDeviceLink-Example-ObjC.xcscheme index ed5b74e00..52405b71f 100644 --- a/SmartDeviceLink-iOS.xcodeproj/xcshareddata/xcschemes/SmartDeviceLink-Example-ObjC.xcscheme +++ b/SmartDeviceLink-iOS.xcodeproj/xcshareddata/xcschemes/SmartDeviceLink-Example-ObjC.xcscheme @@ -1,6 +1,6 @@ *)oldMenuCells updatedMenuCells:(NSArray *)updatedMenuCells; ++ (SDLDynamicMenuUpdateRunScore *)dynamicRunScoreOldMenuCells:(NSArray *)oldMenuCells updatedMenuCells:(NSArray *)updatedMenuCells; + ++ (SDLDynamicMenuUpdateRunScore *)compatibilityRunScoreWithOldMenuCells:(NSArray *)oldMenuCells updatedMenuCells:(NSArray *)updatedMenuCells; @end diff --git a/SmartDeviceLink/private/SDLDynamicMenuUpdateAlgorithm.m b/SmartDeviceLink/private/SDLDynamicMenuUpdateAlgorithm.m index 51bfd14e2..23a500988 100644 --- a/SmartDeviceLink/private/SDLDynamicMenuUpdateAlgorithm.m +++ b/SmartDeviceLink/private/SDLDynamicMenuUpdateAlgorithm.m @@ -10,40 +10,50 @@ #import "SDLDynamicMenuUpdateRunScore.h" #import "SDLMenuCell.h" #import "SDLLogMacros.h" -#import "SDLMenuManagerConstants.h" +#import "SDLWindowCapability.h" NS_ASSUME_NONNULL_BEGIN @implementation SDLDynamicMenuUpdateAlgorithm -#pragma mark - Update Menu Cells -+ (nullable SDLDynamicMenuUpdateRunScore *)compareOldMenuCells:(NSArray *)oldMenuCells updatedMenuCells:(NSArray *)updatedMenuCells{ +#pragma mark Compatibility Menu Run Score + ++ (SDLDynamicMenuUpdateRunScore *)compatibilityRunScoreWithOldMenuCells:(NSArray *)oldMenuCells updatedMenuCells:(NSArray *)updatedMenuCells { + return [[SDLDynamicMenuUpdateRunScore alloc] initWithOldStatus:[self sdl_buildAllDeleteStatusesForMenu:oldMenuCells] updatedStatus:[self sdl_buildAllAddStatusesForMenu:updatedMenuCells] score:updatedMenuCells.count]; +} + +#pragma mark - Dynamic Menu Run Score + ++ (SDLDynamicMenuUpdateRunScore *)dynamicRunScoreOldMenuCells:(NSArray *)oldMenuCells updatedMenuCells:(NSArray *)updatedMenuCells { if (oldMenuCells.count > 0 && updatedMenuCells.count == 0) { - return [[SDLDynamicMenuUpdateRunScore alloc] initWithOldStatus:[SDLDynamicMenuUpdateAlgorithm sdl_buildAllDeleteStatusesforMenu:oldMenuCells] updatedStatus:@[] score:0]; + // Deleting all cells + return [[SDLDynamicMenuUpdateRunScore alloc] initWithOldStatus:[SDLDynamicMenuUpdateAlgorithm sdl_buildAllDeleteStatusesForMenu:oldMenuCells] updatedStatus:@[] score:0]; }else if (oldMenuCells.count == 0 && updatedMenuCells.count > 0) { + // No cells to delete return [[SDLDynamicMenuUpdateRunScore alloc] initWithOldStatus:@[] updatedStatus:[SDLDynamicMenuUpdateAlgorithm sdl_buildAllAddStatusesForMenu:updatedMenuCells] score:updatedMenuCells.count]; } else if (oldMenuCells.count == 0 && updatedMenuCells.count == 0) { - return nil; + // Empty menu to empty menu + return [[SDLDynamicMenuUpdateRunScore alloc] initWithOldStatus:@[] updatedStatus:@[] score:0]; } return [SDLDynamicMenuUpdateAlgorithm sdl_startCompareAtRun:0 oldMenuCells:oldMenuCells updatedMenuCells:updatedMenuCells]; } -+ (nullable SDLDynamicMenuUpdateRunScore *)sdl_startCompareAtRun:(NSUInteger)startRun oldMenuCells:(NSArray *)oldMenuCells updatedMenuCells:(NSArray *)updatedMenuCells { - SDLDynamicMenuUpdateRunScore *bestScore = nil; ++ (SDLDynamicMenuUpdateRunScore *)sdl_startCompareAtRun:(NSUInteger)startRun oldMenuCells:(NSArray *)oldMenuCells updatedMenuCells:(NSArray *)updatedMenuCells { + SDLDynamicMenuUpdateRunScore *bestScore = [[SDLDynamicMenuUpdateRunScore alloc] initWithOldStatus:@[] updatedStatus:@[] score:0]; for (NSUInteger run = startRun; run < oldMenuCells.count; run++) { // Set the menu status as a 1-1 array, start off will oldMenus = all Deletes, newMenu = all Adds - NSMutableArray *oldMenuStatus = [SDLDynamicMenuUpdateAlgorithm sdl_buildAllDeleteStatusesforMenu:oldMenuCells]; + NSMutableArray *oldMenuStatus = [SDLDynamicMenuUpdateAlgorithm sdl_buildAllDeleteStatusesForMenu:oldMenuCells]; NSMutableArray *newMenuStatus = [SDLDynamicMenuUpdateAlgorithm sdl_buildAllAddStatusesForMenu:updatedMenuCells]; NSUInteger startIndex = 0; for (NSUInteger oldCellIndex = run; oldCellIndex < oldMenuCells.count; oldCellIndex++) { //For each old item // Create inner loop to compare old cells to new cells to find a match, if a match if found we mark the index at match for both the old and the new status to keep since we do not want to send RPCs for those cases for (NSUInteger newCellIndex = startIndex; newCellIndex < updatedMenuCells.count; newCellIndex++) { - if ([oldMenuCells[oldCellIndex] isEqual:updatedMenuCells[newCellIndex]]) { - oldMenuStatus[oldCellIndex] = @(MenuCellStateKeep); - newMenuStatus[newCellIndex] = @(MenuCellStateKeep); + if ([oldMenuCells[oldCellIndex] isEqualToCellWithUniqueTitle:updatedMenuCells[newCellIndex]]) { + oldMenuStatus[oldCellIndex] = @(SDLMenuCellUpdateStateKeep); + newMenuStatus[newCellIndex] = @(SDLMenuCellUpdateStateKeep); startIndex = newCellIndex + 1; break; } @@ -54,18 +64,17 @@ + (nullable SDLDynamicMenuUpdateRunScore *)sdl_startCompareAtRun:(NSUInteger)sta NSUInteger numberOfAdds = 0; for (NSUInteger status = 0; status < newMenuStatus.count; status++) { // 0 = Delete 1 = Add 2 = Keep - if (newMenuStatus[status].integerValue == MenuCellStateAdd) { + if (newMenuStatus[status].integerValue == SDLMenuCellUpdateStateAdd) { numberOfAdds++; } } // As soon as we a run that requires 0 Adds we will use it since we cant do better then 0 if (numberOfAdds == 0) { - bestScore = [[SDLDynamicMenuUpdateRunScore alloc] initWithOldStatus:oldMenuStatus updatedStatus:newMenuStatus score:numberOfAdds]; - return bestScore; + return [[SDLDynamicMenuUpdateRunScore alloc] initWithOldStatus:oldMenuStatus updatedStatus:newMenuStatus score:numberOfAdds]; } - // if we havent create the bestScore object or if the current score beats the old score then we will create a new bestScore - if (bestScore == nil || numberOfAdds < bestScore.score) { + // if we haven't create the bestScore object or if the current score beats the old score then we will create a new bestScore + if (bestScore.isEmpty || numberOfAdds < bestScore.score) { bestScore = [[SDLDynamicMenuUpdateRunScore alloc] initWithOldStatus:oldMenuStatus updatedStatus:newMenuStatus score:numberOfAdds]; } } @@ -78,12 +87,12 @@ + (nullable SDLDynamicMenuUpdateRunScore *)sdl_startCompareAtRun:(NSUInteger)sta @param oldMenu The old menu array */ -+ (NSMutableArray *)sdl_buildAllDeleteStatusesforMenu:(NSArray *)oldMenu { - NSMutableArray *oldMenuStatus = [[NSMutableArray alloc] init]; ++ (NSMutableArray *)sdl_buildAllDeleteStatusesForMenu:(NSArray *)oldMenu { + NSMutableArray *oldMenuStatus = [[NSMutableArray alloc] initWithCapacity:oldMenu.count]; for (NSUInteger index = 0; index < oldMenu.count; index++) { - [oldMenuStatus addObject:@(MenuCellStateDelete)]; + [oldMenuStatus addObject:@(SDLMenuCellUpdateStateDelete)]; } - return [oldMenuStatus mutableCopy]; + return oldMenuStatus; } /** @@ -92,11 +101,11 @@ + (nullable SDLDynamicMenuUpdateRunScore *)sdl_startCompareAtRun:(NSUInteger)sta @param newMenu The new menu array */ + (NSMutableArray *)sdl_buildAllAddStatusesForMenu:(NSArray *)newMenu { - NSMutableArray *newMenuStatus = [[NSMutableArray alloc] init]; + NSMutableArray *newMenuStatus = [[NSMutableArray alloc] initWithCapacity:newMenu.count]; for (NSUInteger index = 0; index < newMenu.count; index++) { - [newMenuStatus addObject:@(MenuCellStateAdd)]; + [newMenuStatus addObject:@(SDLMenuCellUpdateStateAdd)]; } - return [newMenuStatus mutableCopy]; + return newMenuStatus; } @end diff --git a/SmartDeviceLink/private/SDLDynamicMenuUpdateRunScore.h b/SmartDeviceLink/private/SDLDynamicMenuUpdateRunScore.h index 12f251970..5ec70a6d8 100644 --- a/SmartDeviceLink/private/SDLDynamicMenuUpdateRunScore.h +++ b/SmartDeviceLink/private/SDLDynamicMenuUpdateRunScore.h @@ -13,12 +13,12 @@ NS_ASSUME_NONNULL_BEGIN @interface SDLDynamicMenuUpdateRunScore : NSObject /** - Will contain all the Deletes and Keeps + Will contain all the Deletes and Keeps. Contains SDLMenuState. */ @property (copy, nonatomic, readonly) NSArray *oldStatus; /** - Will contain all the Adds and Keeps + Will contain all the Adds and Keeps. Contains SDLMenuState. */ @property (copy, nonatomic, readonly) NSArray *updatedStatus; @@ -27,6 +27,9 @@ NS_ASSUME_NONNULL_BEGIN */ @property (assign, nonatomic, readonly) NSUInteger score; +/// Contains no old score, new score, or score +@property (assign, nonatomic, readonly) BOOL isEmpty; + - (instancetype)initWithOldStatus:(NSArray *)oldStatus updatedStatus:(NSArray *)updatedStatus score:(NSUInteger)score; @end diff --git a/SmartDeviceLink/private/SDLDynamicMenuUpdateRunScore.m b/SmartDeviceLink/private/SDLDynamicMenuUpdateRunScore.m index 5066904a5..c5f8b8608 100644 --- a/SmartDeviceLink/private/SDLDynamicMenuUpdateRunScore.m +++ b/SmartDeviceLink/private/SDLDynamicMenuUpdateRunScore.m @@ -8,6 +8,8 @@ #import "SDLDynamicMenuUpdateRunScore.h" +#import "SDLDynamicMenuUpdateAlgorithm.h" + NS_ASSUME_NONNULL_BEGIN @implementation SDLDynamicMenuUpdateRunScore @@ -23,6 +25,29 @@ - (instancetype)initWithOldStatus:(NSArray *)oldStatus updatedStatus return self; } +- (NSString *)description { + return [NSString stringWithFormat:@"Run Score: %ld, old status: %@, updated status: %@", (long)self.score, [self sdl_stringArrayForCellUpdateStatuses:self.oldStatus], [self sdl_stringArrayForCellUpdateStatuses:self.updatedStatus]]; +} + +- (NSArray *)sdl_stringArrayForCellUpdateStatuses:(NSArray *)statuses { + NSMutableArray *mutableStringArray = [NSMutableArray arrayWithCapacity:statuses.count]; + for (NSNumber *status in statuses) { + if (status.unsignedIntegerValue == SDLMenuCellUpdateStateDelete) { + [mutableStringArray addObject:@"DELETE"]; + } else if (status.unsignedIntegerValue == SDLMenuCellUpdateStateAdd) { + [mutableStringArray addObject:@"ADD"]; + } else if (status.unsignedIntegerValue == SDLMenuCellUpdateStateKeep) { + [mutableStringArray addObject:@"KEEP"]; + } + } + + return [mutableStringArray copy]; +} + +- (BOOL)isEmpty { + return (self.oldStatus.count == 0 && self.updatedStatus.count == 0 && self.score == 0); +} + @end NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLink/private/SDLError.h b/SmartDeviceLink/private/SDLError.h index 86abd7d5f..7baabc7d1 100644 --- a/SmartDeviceLink/private/SDLError.h +++ b/SmartDeviceLink/private/SDLError.h @@ -12,6 +12,9 @@ #import "SDLErrorConstants.h" #import "SDLResult.h" +@class SDLMenuCell; +@class SDLMenuConfiguration; + NS_ASSUME_NONNULL_BEGIN @@ -56,7 +59,13 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark Menu Manager ++ (NSError *)sdl_menuManager_configurationOperationLayoutsNotSupported; ++ (NSError *)sdl_menuManager_configurationOperationFailed:(SDLMenuConfiguration *)failedConfiguration; ++ (NSError *)sdl_menuManager_openMenuOperationCancelled; ++ (NSError *)sdl_menuManager_openMenuOperationFailed:(nullable SDLMenuCell *)menuCell; ++ (NSError *)sdl_menuManager_replaceOperationCancelled; + (NSError *)sdl_menuManager_failedToUpdateWithDictionary:(NSDictionary *)userInfo; + + (NSError *)sdl_voiceCommandManager_pendingUpdateSuperseded; #pragma mark Choice Set Manager diff --git a/SmartDeviceLink/private/SDLError.m b/SmartDeviceLink/private/SDLError.m index 568e0f34f..a1dc6b52d 100644 --- a/SmartDeviceLink/private/SDLError.m +++ b/SmartDeviceLink/private/SDLError.m @@ -9,6 +9,7 @@ #import "SDLError.h" #import "SDLChoiceSetManager.h" +#import "SDLMenuConfiguration.h" NS_ASSUME_NONNULL_BEGIN @@ -270,6 +271,54 @@ + (NSError *)sdl_subscribeButtonManager_notSubscribed { #pragma mark Menu Manager ++ (NSError *)sdl_menuManager_configurationOperationLayoutsNotSupported { + return [NSError errorWithDomain:SDLErrorDomainMenuManager code:SDLMenuManagerErrorConfigurationUpdateLayoutNotSupported userInfo:@{ + NSLocalizedDescriptionKey: @"Menu Manager - Configuration Update Failed", + NSLocalizedFailureReasonErrorKey: @"One or more of the configuration layouts is not supported by the module", + NSLocalizedRecoverySuggestionErrorKey: @"Compare SDLManager.systemCapabilityManager.defaultWindowCapability.menuLayoutsAvailable to what you attempted to set" + }]; +} + ++ (NSError *)sdl_menuManager_configurationOperationFailed:(SDLMenuConfiguration *)failedConfiguration { + return [NSError errorWithDomain:SDLErrorDomainMenuManager code:SDLMenuManagerErrorConfigurationUpdateFailed userInfo:@{ + @"Failed Configuration": failedConfiguration, + NSLocalizedDescriptionKey: @"Menu Manager - Configuration Update Failed", + NSLocalizedFailureReasonErrorKey: @"The configuration may not be supported by the connected head unit", + NSLocalizedRecoverySuggestionErrorKey: @"Check SystemCapabilityManager.defaultWindowCapability.menuLayouts to ensure the set configuration is supported" + }]; +} + ++ (NSError *)sdl_menuManager_openMenuOperationCancelled { + return [NSError errorWithDomain:SDLErrorDomainMenuManager code:SDLMenuManagerErrorOperationCancelled userInfo:@{ + NSLocalizedDescriptionKey: @"Menu Manager - Open Menu Cancelled", + NSLocalizedFailureReasonErrorKey: @"The menu manager was probably stopped or opening another menu item was requested.", + NSLocalizedRecoverySuggestionErrorKey: @"This error probably does not need recovery." + }]; +} + ++ (NSError *)sdl_menuManager_openMenuOperationFailed:(nullable SDLMenuCell *)menuCell { + NSString *failureReason = nil; + if (menuCell != nil) { + failureReason = @"Something went wrong attempting to open the menu."; + } else { + failureReason = [NSString stringWithFormat:@"Something went wrong attempting to open the menu to the given subcell: %@", menuCell]; + } + + return [NSError errorWithDomain:SDLErrorDomainMenuManager code:SDLMenuManagerErrorOpenMenuFailed userInfo:@{ + NSLocalizedDescriptionKey: @"Menu Manager - Open Menu Failed", + NSLocalizedFailureReasonErrorKey: failureReason, + NSLocalizedRecoverySuggestionErrorKey: @"Check the error logs for more information on the RPC failure." + }]; +} + ++ (NSError *)sdl_menuManager_replaceOperationCancelled { + return [NSError errorWithDomain:SDLErrorDomainMenuManager code:SDLMenuManagerErrorOperationCancelled userInfo:@{ + NSLocalizedDescriptionKey: @"Menu Manager - Menu Replace Cancelled", + NSLocalizedFailureReasonErrorKey: @"The menu manager was probably stopped or another menu update was requested.", + NSLocalizedRecoverySuggestionErrorKey: @"This error probably does not need recovery." + }]; +} + + (NSError *)sdl_menuManager_failedToUpdateWithDictionary:(NSDictionary *)userInfo { return [NSError errorWithDomain:SDLErrorDomainMenuManager code:SDLMenuManagerErrorRPCsFailed userInfo:userInfo]; } diff --git a/SmartDeviceLink/private/SDLLifecycleManager.m b/SmartDeviceLink/private/SDLLifecycleManager.m index 0e23d1cc5..826a91dff 100644 --- a/SmartDeviceLink/private/SDLLifecycleManager.m +++ b/SmartDeviceLink/private/SDLLifecycleManager.m @@ -681,8 +681,7 @@ - (void)sendRequest:(SDLRPCRequest *)request withResponseHandler:(nullable SDLRe - (void)sendRequests:(NSArray *)requests progressHandler:(nullable SDLMultipleAsyncRequestProgressHandler)progressHandler completionHandler:(nullable SDLMultipleRequestCompletionHandler)completionHandler { if (requests.count == 0) { - completionHandler(YES); - return; + return completionHandler(YES); } SDLAsynchronousRPCRequestOperation *op = [[SDLAsynchronousRPCRequestOperation alloc] initWithConnectionManager:self requests:requests progressHandler:progressHandler completionHandler:completionHandler]; diff --git a/SmartDeviceLink/private/SDLLogFileModuleMap.m b/SmartDeviceLink/private/SDLLogFileModuleMap.m index eed4a776b..d8da4cdb2 100644 --- a/SmartDeviceLink/private/SDLLogFileModuleMap.m +++ b/SmartDeviceLink/private/SDLLogFileModuleMap.m @@ -135,7 +135,7 @@ + (SDLLogFileModule *)sdl_screenManagerAlertModule { } + (SDLLogFileModule *)sdl_screenManagerMenuModule { - return [SDLLogFileModule moduleWithName:@"Screen/Menu" files:[NSSet setWithArray:@[@"SDLMenuManager", @"SDLVoiceCommandManager", @"SDLVoiceCommandUpdateOperation"]]]; + return [SDLLogFileModule moduleWithName:@"Screen/Menu" files:[NSSet setWithArray:@[@"SDLMenuManager", @"SDLMenuReplaceOperation", @"SDLMenuShowOperation", @"SDLMenuConfigurationUpdateOperation", @"SDLVoiceCommandManager", @"SDLVoiceCommandUpdateOperation"]]]; } + (SDLLogFileModule *)sdl_screenManagerChoiceSetModule { diff --git a/SmartDeviceLink/private/SDLMenuConfigurationUpdateOperation.h b/SmartDeviceLink/private/SDLMenuConfigurationUpdateOperation.h new file mode 100644 index 000000000..9482ccfe1 --- /dev/null +++ b/SmartDeviceLink/private/SDLMenuConfigurationUpdateOperation.h @@ -0,0 +1,26 @@ +// +// SDLMenuConfigurationUpdateOperation.h +// SmartDeviceLink +// +// Created by Joel Fischer on 1/21/21. +// Copyright © 2021 smartdevicelink. All rights reserved. +// + +#import "SDLAsynchronousOperation.h" + +#import "SDLConnectionManagerType.h" +#import "SDLMenuConfiguration.h" +#import "SDLMenuReplaceUtilities.h" +#import "SDLWindowCapability.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef void(^SDLMenuConfigurationUpdatedBlock)(SDLMenuConfiguration *_Nullable newMenuConfiguration, NSError *_Nullable error); + +@interface SDLMenuConfigurationUpdateOperation : SDLAsynchronousOperation + +- (instancetype)initWithConnectionManager:(id)connectionManager windowCapability:(SDLWindowCapability *)windowCapability newMenuConfiguration:(SDLMenuConfiguration *)newConfiguration configurationUpdatedHandler:(SDLMenuConfigurationUpdatedBlock)configurationUpdatedBlock; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLink/private/SDLMenuConfigurationUpdateOperation.m b/SmartDeviceLink/private/SDLMenuConfigurationUpdateOperation.m new file mode 100644 index 000000000..39482643f --- /dev/null +++ b/SmartDeviceLink/private/SDLMenuConfigurationUpdateOperation.m @@ -0,0 +1,99 @@ +// +// SDLMenuConfigurationUpdateOperation.m +// SmartDeviceLink +// +// Created by Joel Fischer on 1/21/21. +// Copyright © 2021 smartdevicelink. All rights reserved. +// + +#import "SDLMenuConfigurationUpdateOperation.h" + +#import "SDLError.h" +#import "SDLGlobals.h" +#import "SDLLogMacros.h" +#import "SDLSetGlobalProperties.h" +#import "SDLVersion.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface SDLMenuConfigurationUpdateOperation () + +@property (weak, nonatomic) id connectionManager; +@property (strong, nonatomic) NSArray *availableMenuLayouts; +@property (strong, nonatomic) SDLMenuConfiguration *updatedMenuConfiguration; +@property (assign, nonatomic) SDLMenuConfigurationUpdatedBlock menuConfigurationUpdatedBlock; + +@property (copy, nonatomic, nullable) NSError *internalError; + +@end + +@implementation SDLMenuConfigurationUpdateOperation + +- (instancetype)initWithConnectionManager:(id)connectionManager windowCapability:(SDLWindowCapability *)windowCapability newMenuConfiguration:(SDLMenuConfiguration *)newConfiguration configurationUpdatedHandler:(SDLMenuConfigurationUpdatedBlock)configurationUpdatedBlock { + self = [super init]; + if (!self) { return nil; } + + _connectionManager = connectionManager; + _availableMenuLayouts = windowCapability.menuLayoutsAvailable; + _updatedMenuConfiguration = newConfiguration; + _menuConfigurationUpdatedBlock = configurationUpdatedBlock; + + return self; +} + +- (void)start { + [super start]; + if (self.isCancelled) { return; } + + if (self.availableMenuLayouts.count == 0) { + SDLLogW(@"Could not set the main menu configuration. Which menu layouts can be used is not available"); + self.internalError = [NSError sdl_menuManager_configurationOperationLayoutsNotSupported]; + return [self finishOperation]; + } else if (![self.availableMenuLayouts containsObject:self.updatedMenuConfiguration.mainMenuLayout] + || ![self.availableMenuLayouts containsObject:self.updatedMenuConfiguration.defaultSubmenuLayout]) { + SDLLogE(@"One or more of the set menu layouts are not available on this system. The menu configuration will not be set. Available menu layouts: %@, set menu layouts: %@", self.availableMenuLayouts, self.updatedMenuConfiguration); + self.internalError = [NSError sdl_menuManager_configurationOperationLayoutsNotSupported]; + return [self finishOperation]; + } + + __weak typeof(self) weakself = self; + SDLSetGlobalProperties *setGlobalsRPC = [[SDLSetGlobalProperties alloc] init]; + setGlobalsRPC.menuLayout = self.updatedMenuConfiguration.mainMenuLayout; + [self.connectionManager sendConnectionRequest:setGlobalsRPC withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) { + __strong typeof(weakself) strongself = weakself; + if (error != nil) { + strongself.internalError = [NSError sdl_menuManager_configurationOperationFailed:strongself.updatedMenuConfiguration]; + } + + [strongself finishOperation]; + }]; +} + +#pragma mark - Operation Overrides + +- (void)finishOperation { + SDLLogV(@"Finishing menu manager configuration update operation"); + if (self.internalError != nil) { + self.menuConfigurationUpdatedBlock(nil, self.internalError); + } else { + self.menuConfigurationUpdatedBlock(self.updatedMenuConfiguration, nil); + } + + [super finishOperation]; +} + +- (nullable NSString *)name { + return @"com.sdl.menuManager.configurationUpdate"; +} + +- (NSOperationQueuePriority)queuePriority { + return NSOperationQueuePriorityNormal; +} + +- (nullable NSError *)error { + return self.internalError; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLink/private/SDLMenuManager.h b/SmartDeviceLink/private/SDLMenuManager.h index a17775131..806e21a4a 100644 --- a/SmartDeviceLink/private/SDLMenuManager.h +++ b/SmartDeviceLink/private/SDLMenuManager.h @@ -42,9 +42,7 @@ typedef void(^SDLMenuUpdateCompletionHandler)(NSError *__nullable error); @property (assign, nonatomic) SDLDynamicMenuUpdatesMode dynamicMenuUpdatesMode; -- (BOOL)openMenu; - -- (BOOL)openSubmenu:(SDLMenuCell *)cell; +- (BOOL)openMenu:(nullable SDLMenuCell *)cell; @end diff --git a/SmartDeviceLink/private/SDLMenuManager.m b/SmartDeviceLink/private/SDLMenuManager.m index 7f05a2c8c..e8d7ef8a4 100644 --- a/SmartDeviceLink/private/SDLMenuManager.m +++ b/SmartDeviceLink/private/SDLMenuManager.m @@ -16,6 +16,8 @@ #import "SDLDeleteSubMenu.h" #import "SDLDisplayCapability.h" #import "SDLDisplayType.h" +#import "SDLDynamicMenuUpdateRunScore.h" +#import "SDLDynamicMenuUpdateAlgorithm.h" #import "SDLError.h" #import "SDLFileManager.h" #import "SDLGlobals.h" @@ -23,9 +25,11 @@ #import "SDLLogMacros.h" #import "SDLMenuCell.h" #import "SDLMenuConfiguration.h" +#import "SDLMenuConfigurationUpdateOperation.h" +#import "SDLMenuManagerPrivateConstants.h" #import "SDLMenuParams.h" -#import "SDLDynamicMenuUpdateRunScore.h" -#import "SDLDynamicMenuUpdateAlgorithm.h" +#import "SDLMenuReplaceOperation.h" +#import "SDLMenuShowOperation.h" #import "SDLOnCommand.h" #import "SDLOnHMIStatus.h" #import "SDLPredefinedWindows.h" @@ -68,34 +72,27 @@ @interface SDLMenuManager() @property (weak, nonatomic) SDLFileManager *fileManager; @property (weak, nonatomic) SDLSystemCapabilityManager *systemCapabilityManager; -@property (copy, nonatomic, nullable) SDLHMILevel currentHMILevel; -@property (copy, nonatomic, nullable) SDLSystemContext currentSystemContext; - -@property (strong, nonatomic, nullable) NSArray *inProgressUpdate; -@property (assign, nonatomic) BOOL hasQueuedUpdate; -@property (assign, nonatomic) BOOL waitingOnHMIUpdate; -@property (copy, nonatomic) NSArray *waitingUpdateMenuCells; +@property (strong, nonatomic) NSOperationQueue *transactionQueue; @property (strong, nonatomic, nullable) SDLWindowCapability *windowCapability; -@property (assign, nonatomic) UInt32 lastMenuId; -@property (copy, nonatomic) NSArray *oldMenuCells; +@property (copy, nonatomic, nullable) SDLHMILevel currentHMILevel; +@property (copy, nonatomic, nullable) SDLSystemContext currentSystemContext; +@property (copy, nonatomic) NSArray *currentMenuCells; +@property (strong, nonatomic, nullable) SDLMenuConfiguration *currentMenuConfiguration; @end -UInt32 const ParentIdNotFound = UINT32_MAX; -UInt32 const MenuCellIdMin = 1; - @implementation SDLMenuManager - (instancetype)init { self = [super init]; if (!self) { return nil; } - _lastMenuId = MenuCellIdMin; _menuConfiguration = [[SDLMenuConfiguration alloc] init]; _menuCells = @[]; - _oldMenuCells = @[]; + _currentMenuCells = @[]; _dynamicMenuUpdatesMode = SDLDynamicMenuUpdatesModeOnWithCompatibility; + _transactionQueue = [self sdl_newTransactionQueue]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sdl_hmiStatusNotification:) name:SDLDidChangeHMIStatusNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sdl_commandNotification:) name:SDLDidReceiveCommandNotification object:nil]; @@ -119,398 +116,161 @@ - (void)start { } - (void)stop { - _lastMenuId = MenuCellIdMin; _menuCells = @[]; - _oldMenuCells = @[]; + _currentMenuCells = @[]; + _transactionQueue = [self sdl_newTransactionQueue]; _currentHMILevel = nil; - _currentSystemContext = SDLSystemContextMain; - _inProgressUpdate = nil; - _hasQueuedUpdate = NO; - _waitingOnHMIUpdate = NO; - _waitingUpdateMenuCells = @[]; + _currentSystemContext = nil; + _currentMenuConfiguration = nil; + _windowCapability = nil; +} + +#pragma mark Transaction Queue + +- (NSOperationQueue *)sdl_newTransactionQueue { + NSOperationQueue *queue = [[NSOperationQueue alloc] init]; + queue.name = @"SDLMenuManager Transaction Queue"; + queue.maxConcurrentOperationCount = 1; + queue.qualityOfService = NSQualityOfServiceUserInitiated; + queue.underlyingQueue = [SDLGlobals sharedGlobals].sdlConcurrentQueue; + queue.suspended = YES; + + return queue; +} + +/// Suspend the queue if the HMI level is NONE since we want to delay sending RPCs until we're in non-NONE +- (void)sdl_updateTransactionQueueSuspended { + if ([self.currentHMILevel isEqualToEnum:SDLHMILevelNone] || [self.currentSystemContext isEqualToEnum:SDLSystemContextMenu]) { + SDLLogD(@"Suspending the transaction queue. Current HMI level is NONE: %@, current system context is MENU: %@", ([self.currentHMILevel isEqualToEnum:SDLHMILevelNone] ? @"YES" : @"NO"), ([self.currentSystemContext isEqualToEnum:SDLSystemContextMenu] ? @"YES" : @"NO")); + self.transactionQueue.suspended = YES; + } else { + SDLLogD(@"Starting the transaction queue"); + self.transactionQueue.suspended = NO; + } } #pragma mark - Setters - (void)setMenuConfiguration:(SDLMenuConfiguration *)menuConfiguration { - NSArray *layoutsAvailable = self.windowCapability.menuLayoutsAvailable; - - if ([[SDLGlobals sharedGlobals].rpcVersion isLessThanVersion:[SDLVersion versionWithMajor:6 minor:0 patch:0]]) { - SDLLogW(@"Menu configurations is only supported on head units with RPC spec version 6.0.0 or later. Currently connected head unit RPC spec version is %@", [SDLGlobals sharedGlobals].rpcVersion); - return; - } else if (layoutsAvailable == nil) { - SDLLogW(@"Could not set the main menu configuration. Which menu layouts can be used is not available"); - return; - } else if (![layoutsAvailable containsObject:menuConfiguration.mainMenuLayout] - || ![layoutsAvailable containsObject:menuConfiguration.defaultSubmenuLayout]) { - SDLLogE(@"One or more of the set menu layouts are not available on this system. The menu configuration will not be set. Available menu layouts: %@, set menu layouts: %@", layoutsAvailable, menuConfiguration); + if ([menuConfiguration isEqual:self.menuConfiguration]) { + SDLLogD(@"New menu configuration is equal to existing one, will not set new configuration"); return; - } else if (self.currentHMILevel == nil - || [self.currentHMILevel isEqualToEnum:SDLHMILevelNone]) { - SDLLogE(@"Could not set main menu configuration, HMI level: %@, required: 'Not-NONE', system context: %@, required: 'Not MENU'", self.currentHMILevel, self.currentSystemContext); + } else if ([[SDLGlobals sharedGlobals].rpcVersion isLessThanVersion:[SDLVersion versionWithMajor:6 minor:0 patch:0]]) { + SDLLogE(@"Setting a menu configuration is not supported on this head unit. Only supported on RPC 6.0+, this version: %@", [SDLGlobals sharedGlobals].rpcVersion); return; } - SDLMenuConfiguration *oldConfig = _menuConfiguration; _menuConfiguration = menuConfiguration; - SDLSetGlobalProperties *setGlobalsRPC = [[SDLSetGlobalProperties alloc] init]; - setGlobalsRPC.menuLayout = menuConfiguration.mainMenuLayout; - + // Create the operation __weak typeof(self) weakself = self; - [self.connectionManager sendConnectionRequest:setGlobalsRPC withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) { - __strong typeof(weakself) strongself = weakself; + SDLMenuConfigurationUpdateOperation *configurationUpdateOp = [[SDLMenuConfigurationUpdateOperation alloc] initWithConnectionManager:self.connectionManager windowCapability:self.windowCapability newMenuConfiguration:menuConfiguration configurationUpdatedHandler:^(SDLMenuConfiguration *newMenuConfiguration, NSError *_Nullable error) { if (error != nil) { - SDLLogE(@"Could not set main menu configuration: %@", error); - strongself.menuConfiguration = oldConfig; + SDLLogE(@"Error updating menu configuration: %@", error); return; } + weakself.currentMenuConfiguration = newMenuConfiguration; + [weakself sdl_updateMenuReplaceOperationsWithNewMenuConfiguration]; }]; + + // Cancel previous menu configuration operations + for (NSOperation *operation in self.transactionQueue.operations) { + if ([operation isMemberOfClass:[SDLMenuConfigurationUpdateOperation class]]) { + [operation cancel]; + } + } + + // Add the new menu configuration operation to the queue + [self.transactionQueue addOperation:configurationUpdateOp]; } - (void)setMenuCells:(NSArray *)menuCells { - NSArray *menuCellsCopy = [[NSArray alloc] initWithArray:menuCells copyItems:YES]; - // 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:menuCellsCopy allVoiceCommands:[NSMutableArray array]]) { return; } - - if (self.currentHMILevel == nil - || [self.currentHMILevel isEqualToEnum:SDLHMILevelNone] - || [self.currentSystemContext isEqualToEnum:SDLSystemContextMenu]) { - SDLLogD(@"Waiting for HMI update to send menu cells"); - self.waitingOnHMIUpdate = YES; - self.waitingUpdateMenuCells = menuCells; + if (![self sdl_menuCellsAreUnique:menuCells allVoiceCommands:[NSMutableArray array]]) { + SDLLogE(@"Not all set menu cells are unique, but that is required"); return; } - self.waitingOnHMIUpdate = NO; - SDLVersion *menuUniquenessSupportedVersion = [[SDLVersion alloc] initWithMajor:7 minor:1 patch:0]; - if ([[SDLGlobals sharedGlobals].rpcVersion isLessThanVersion:menuUniquenessSupportedVersion]) { - // If we're on < RPC 7.1, all primary texts need to be unique, so we don't need to check removed properties and duplicate cells - [self sdl_addUniqueNamesToCellsWithDuplicatePrimaryText:menuCellsCopy]; - } else { - // On > RPC 7.1, at this point all cells are unique when considering all properties, but we also need to check if any cells will _appear_ as duplicates when displayed on the screen. To check that, we'll remove properties from the set cells based on the system capabilities (we probably don't need to consider them changing between now and when they're actually sent to the HU unless the menu layout changes) and check for uniqueness again. Then we'll add unique identifiers to primary text if there are duplicates. Then we transfer the primary text identifiers back to the main cells and add those to an operation to be sent. - NSArray *strippedCellsCopy = [self sdl_removeUnusedProperties:menuCellsCopy]; - [self sdl_addUniqueNamesBasedOnStrippedCells:strippedCellsCopy toUnstrippedCells:menuCellsCopy]; - } + _menuCells = [[NSArray alloc] initWithArray:menuCells copyItems:YES]; - _oldMenuCells = _menuCells; - _menuCells = menuCellsCopy; + __weak typeof(self) weakself = self; + SDLMenuReplaceOperation *menuReplaceOperation = [[SDLMenuReplaceOperation alloc] initWithConnectionManager:self.connectionManager fileManager:self.fileManager windowCapability:self.windowCapability menuConfiguration:self.currentMenuConfiguration currentMenu:self.currentMenuCells updatedMenu:self.menuCells compatibilityModeEnabled:(![self sdl_isDynamicMenuUpdateActive:self.dynamicMenuUpdatesMode]) currentMenuUpdatedHandler:^(NSArray * _Nonnull currentMenuCells, NSError *error) { + weakself.currentMenuCells = currentMenuCells; + [weakself sdl_updateMenuReplaceOperationsWithNewCurrentMenu]; + SDLLogD(@"Finished updating menu"); + }]; - if ([self sdl_isDynamicMenuUpdateActive:self.dynamicMenuUpdatesMode]) { - [self sdl_startDynamicMenuUpdate]; - } else { - [self sdl_startNonDynamicMenuUpdate]; + // Cancel previous replace menu operations + for (NSOperation *operation in self.transactionQueue.operations) { + if ([operation isMemberOfClass:[SDLMenuReplaceOperation class]]) { + [operation cancel]; + } } + + [self.transactionQueue addOperation:menuReplaceOperation]; } #pragma mark - Open Menu -- (BOOL)openMenu { - if ([SDLGlobals.sharedGlobals.rpcVersion isLessThanVersion:[[SDLVersion alloc] initWithMajor:6 minor:0 patch:0]]) { - SDLLogE(@"The openMenu method is not supported on this head unit."); - return NO; - } - - 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); - } else if (![response.resultCode isEqualToEnum:SDLResultSuccess]) { - SDLLogE(@"Error opening application menu: %@", error); - } else { - SDLLogD(@"Successfully opened application main menu"); - } - }]; - - return YES; -} - -- (BOOL)openSubmenu:(SDLMenuCell *)cell { - if (cell.subCells.count == 0) { +- (BOOL)openMenu:(nullable SDLMenuCell *)cell { + if (cell != nil && cell.subCells.count == 0) { SDLLogE(@"The cell %@ does not contain any sub cells, so no submenu can be opened", cell); return NO; - } else if ([SDLGlobals.sharedGlobals.rpcVersion isLessThanVersion:[[SDLVersion alloc] initWithMajor:6 minor:0 patch:0]]) { - SDLLogE(@"The openSubmenu method is not supported on this head unit."); - return NO; - } else if (![self.menuCells containsObject:cell]) { + } else if (cell != nil && ![self.menuCells containsObject:cell]) { SDLLogE(@"This cell has not been sent to the head unit, so no submenu can be opened. Make sure that the cell exists in the SDLManager.menu array"); return NO; + } else if ([SDLGlobals.sharedGlobals.rpcVersion isLessThanVersion:[[SDLVersion alloc] initWithMajor:6 minor:0 patch:0]]) { + SDLLogE(@"The openMenu / openSubmenu is not supported on this head unit."); + return NO; } - 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); - } else if (![response.resultCode isEqualToEnum:SDLResultSuccess]) { - SDLLogE(@"Error opening application menu to submenu cell %@, with error: %@", cell, error); - } else { - SDLLogD(@"Successfully opened application menu to submenu cell: %@", cell); + // Create the operation + SDLMenuShowOperation *showMenuOp = [[SDLMenuShowOperation alloc] initWithConnectionManager:self.connectionManager toMenuCell:cell completionHandler:^(NSError * _Nullable error) { + if (error != nil) { + SDLLogE(@"Opening menu with error: %@, info: %@. Failed subcell (if nil, attempted to open to main menu): %@", error, error.userInfo, cell); } }]; - return YES; -} - -#pragma mark - Build Deletes, Keeps, Adds - -- (void)sdl_startSubMenuUpdatesWithOldKeptCells:(NSArray *)oldKeptCells newKeptCells:(NSArray *)newKeptCells atIndex:(NSUInteger)startIndex { - if (oldKeptCells.count == 0 || startIndex >= oldKeptCells.count) { - self.inProgressUpdate = nil; - return; - } - - if (oldKeptCells[startIndex].subCells.count > 0) { - SDLDynamicMenuUpdateRunScore *tempScore = [SDLDynamicMenuUpdateAlgorithm compareOldMenuCells:oldKeptCells[startIndex].subCells updatedMenuCells:newKeptCells[startIndex].subCells]; - NSArray *deleteMenuStatus = tempScore.oldStatus; - NSArray *addMenuStatus = tempScore.updatedStatus; - - NSArray *cellsToDelete = [self sdl_filterDeleteMenuItemsWithOldMenuItems:oldKeptCells[startIndex].subCells basedOnStatusList:deleteMenuStatus]; - NSArray *cellsToAdd = [self sdl_filterAddMenuItemsWithNewMenuItems:newKeptCells[startIndex].subCells basedOnStatusList:addMenuStatus]; - - NSArray *oldKeeps = [self sdl_filterKeepMenuItemsWithOldMenuItems:oldKeptCells[startIndex].subCells basedOnStatusList:deleteMenuStatus]; - NSArray *newKeeps = [self sdl_filterKeepMenuItemsWithNewMenuItems:newKeptCells[startIndex].subCells basedOnStatusList:addMenuStatus]; - - [self sdl_updateIdsOnMenuCells:cellsToAdd parentId:newKeptCells[startIndex].cellId]; - [self transferCellIDFromOldCells:oldKeeps toKeptCells:newKeeps]; - - __weak typeof(self) weakself = self; - [self sdl_sendDeleteCurrentMenu:cellsToDelete withCompletionHandler:^(NSError * _Nullable error) { - [weakself sdl_sendUpdatedMenu:cellsToAdd usingMenu:weakself.menuCells[startIndex].subCells withCompletionHandler:^(NSError * _Nullable error) { - // After the first set of submenu cells were added and deleted we must find the next set of subcells untll we loop through all the elemetns - [weakself sdl_startSubMenuUpdatesWithOldKeptCells:oldKeptCells newKeptCells:newKeptCells atIndex:(startIndex + 1)]; - }]; - }]; - } else { - // After the first set of submenu cells were added and deleted we must find the next set of subcells untll we loop through all the elemetns - [self sdl_startSubMenuUpdatesWithOldKeptCells:oldKeptCells newKeptCells:newKeptCells atIndex:(startIndex + 1)]; - } -} - -- (NSArray *)sdl_filterDeleteMenuItemsWithOldMenuItems:(NSArray *)oldMenuCells basedOnStatusList:(NSArray *)oldStatusList { - NSMutableArray *deleteCells = [[NSMutableArray alloc] init]; - // The index of the status should corrleate 1-1 with the number of items in the menu - // [2,0,2,0] => [A,B,C,D] = [B,D] - for (NSUInteger index = 0; index < oldStatusList.count; index++) { - if (oldStatusList[index].integerValue == MenuCellStateDelete) { - [deleteCells addObject:oldMenuCells[index]]; + // Cancel previous open menu operations + for (NSOperation *operation in self.transactionQueue.operations) { + if ([operation isMemberOfClass:[SDLMenuShowOperation class]]) { + [operation cancel]; } } - return [deleteCells copy]; -} - -- (NSArray *)sdl_filterAddMenuItemsWithNewMenuItems:(NSArray *)newMenuCells basedOnStatusList:(NSArray *)newStatusList { - NSMutableArray *addCells = [[NSMutableArray alloc] init]; - // The index of the status should corrleate 1-1 with the number of items in the menu - // [2,1,2,1] => [A,B,C,D] = [B,D] - for (NSUInteger index = 0; index < newStatusList.count; index++) { - if (newStatusList[index].integerValue == MenuCellStateAdd) { - [addCells addObject:newMenuCells[index]]; - } - } - return [addCells copy]; -} -- (NSArray *)sdl_filterKeepMenuItemsWithOldMenuItems:(NSArray *)oldMenuCells basedOnStatusList:(NSArray *)keepStatusList { - NSMutableArray *keepMenuCells = [[NSMutableArray alloc] init]; + // Add the new open menu operation to the queue + [self.transactionQueue addOperation:showMenuOp]; - for (NSUInteger index = 0; index < keepStatusList.count; index++) { - if (keepStatusList[index].integerValue == MenuCellStateKeep) { - [keepMenuCells addObject:oldMenuCells[index]]; - } - } - return [keepMenuCells copy]; -} - -- (NSArray *)sdl_filterKeepMenuItemsWithNewMenuItems:(NSArray *)newMenuCells basedOnStatusList:(NSArray *)keepStatusList { - NSMutableArray *keepMenuCells = [[NSMutableArray alloc] init]; - for (NSUInteger index = 0; index < keepStatusList.count; index++) { - if (keepStatusList[index].integerValue == MenuCellStateKeep) { - [keepMenuCells addObject:newMenuCells[index]]; - } - } - return [keepMenuCells copy]; -} - -- (void)transferCellIDFromOldCells:(NSArray *)oldCells toKeptCells:(NSArray *)newCells { - if (oldCells.count == 0) { return; } - for (NSUInteger i = 0; i < newCells.count; i++) { - newCells[i].cellId = oldCells[i].cellId; - } + return YES; } #pragma mark - Updating System -- (void)sdl_startDynamicMenuUpdate { - SDLDynamicMenuUpdateRunScore *runScore = [SDLDynamicMenuUpdateAlgorithm compareOldMenuCells:self.oldMenuCells updatedMenuCells:self.menuCells]; - - NSArray *deleteMenuStatus = runScore.oldStatus; - NSArray *addMenuStatus = runScore.updatedStatus; - - NSArray *cellsToDelete = [self sdl_filterDeleteMenuItemsWithOldMenuItems:self.oldMenuCells basedOnStatusList:deleteMenuStatus]; - NSArray *cellsToAdd = [self sdl_filterAddMenuItemsWithNewMenuItems:self.menuCells basedOnStatusList:addMenuStatus]; - // These arrays should ONLY contain KEEPS. These will be used for SubMenu compares - NSArray *oldKeeps = [self sdl_filterKeepMenuItemsWithOldMenuItems:self.oldMenuCells basedOnStatusList:deleteMenuStatus]; - NSArray *newKeeps = [self sdl_filterKeepMenuItemsWithNewMenuItems:self.menuCells basedOnStatusList:addMenuStatus]; - - // Cells that will be added need new ids - [self sdl_updateIdsOnMenuCells:cellsToAdd parentId:ParentIdNotFound]; - - // Since we are creating a new Menu but keeping old cells we must firt transfer the old cellIDs to the new menus kept cells. - [self transferCellIDFromOldCells:oldKeeps toKeptCells:newKeeps]; - - // Upload the artworks - NSArray *artworksToBeUploaded = [self sdl_findAllArtworksToBeUploadedFromCells:cellsToAdd]; - if (artworksToBeUploaded.count > 0) { - [self.fileManager uploadArtworks:artworksToBeUploaded completionHandler:^(NSArray * _Nonnull artworkNames, NSError * _Nullable error) { - if (error != nil) { - SDLLogE(@"Error uploading menu artworks: %@", error); - } - SDLLogD(@"Menu artworks uploaded"); - // Update cells with artworks once they're uploaded - __weak typeof(self) weakself = self; - [self sdl_updateMenuWithCellsToDelete:cellsToDelete cellsToAdd:cellsToAdd completionHandler:^(NSError * _Nullable error) { - [weakself sdl_startSubMenuUpdatesWithOldKeptCells:oldKeeps newKeptCells:newKeeps atIndex:0]; - }]; - }]; - } else { - // Cells have no artwork to load - __weak typeof(self) weakself = self; - [self sdl_updateMenuWithCellsToDelete:cellsToDelete cellsToAdd:cellsToAdd completionHandler:^(NSError * _Nullable error) { - [weakself sdl_startSubMenuUpdatesWithOldKeptCells:oldKeeps newKeptCells:newKeeps atIndex:0]; - }]; - } -} - -- (void)sdl_startNonDynamicMenuUpdate { - [self sdl_updateIdsOnMenuCells:self.menuCells parentId:ParentIdNotFound]; - - NSArray *artworksToBeUploaded = [self sdl_findAllArtworksToBeUploadedFromCells:self.menuCells]; - if (artworksToBeUploaded.count > 0) { - [self.fileManager uploadArtworks:artworksToBeUploaded completionHandler:^(NSArray * _Nonnull artworkNames, NSError * _Nullable error) { - if (error != nil) { - SDLLogE(@"Error uploading menu artworks: %@", error); - } - - SDLLogD(@"Menu artworks uploaded"); - [self sdl_updateMenuWithCellsToDelete:self.oldMenuCells cellsToAdd:self.menuCells completionHandler:nil]; - }]; - } else { - // Cells have no artwork to load - [self sdl_updateMenuWithCellsToDelete:self.oldMenuCells cellsToAdd:self.menuCells completionHandler:nil]; +- (void)sdl_updateMenuReplaceOperationsWithNewCurrentMenu { + for (NSOperation *operation in self.transactionQueue.operations) { + if ([operation isMemberOfClass:[SDLMenuReplaceOperation class]]) { + SDLMenuReplaceOperation *op = (SDLMenuReplaceOperation *)operation; + op.currentMenu = self.currentMenuCells; + } } } -- (void)sdl_updateMenuWithCellsToDelete:(NSArray *)deleteCells cellsToAdd:(NSArray *)addCells completionHandler:(nullable SDLMenuUpdateCompletionHandler)completionHandler { - if (self.currentHMILevel == nil - || [self.currentHMILevel isEqualToEnum:SDLHMILevelNone] - || [self.currentSystemContext isEqualToEnum:SDLSystemContextMenu]) { - self.waitingOnHMIUpdate = YES; - self.waitingUpdateMenuCells = self.menuCells; - return; - } - - if (self.inProgressUpdate != nil) { - // There's an in progress update, we need to put this on hold - self.hasQueuedUpdate = YES; - return; +- (void)sdl_updateMenuReplaceOperationsWithNewWindowCapability { + for (NSOperation *operation in self.transactionQueue.operations) { + if ([operation isMemberOfClass:[SDLMenuReplaceOperation class]]) { + SDLMenuReplaceOperation *op = (SDLMenuReplaceOperation *)operation; + op.windowCapability = self.windowCapability; + } } - __weak typeof(self) weakself = self; - [self sdl_sendDeleteCurrentMenu:deleteCells withCompletionHandler:^(NSError * _Nullable error) { - [weakself sdl_sendUpdatedMenu:addCells usingMenu:weakself.menuCells withCompletionHandler:^(NSError * _Nullable error) { - weakself.inProgressUpdate = nil; - - if (completionHandler != nil) { - completionHandler(error); - } - - if (weakself.hasQueuedUpdate) { - [weakself sdl_updateMenuWithCellsToDelete:deleteCells cellsToAdd:addCells completionHandler:nil]; - weakself.hasQueuedUpdate = NO; - } - }]; - }]; } -#pragma mark Delete Old Menu Items - -- (void)sdl_sendDeleteCurrentMenu:(nullable NSArray *)deleteMenuCells withCompletionHandler:(SDLMenuUpdateCompletionHandler)completionHandler { - if (deleteMenuCells.count == 0) { - completionHandler(nil); - return; - } - - NSArray *deleteMenuCommands = [self sdl_deleteCommandsForCells:deleteMenuCells]; - [self.connectionManager sendRequests:deleteMenuCommands progressHandler:nil completionHandler:^(BOOL success) { - if (!success) { - SDLLogW(@"Unable to delete all old menu commands"); - } else { - SDLLogD(@"Finished deleting old menu"); +- (void)sdl_updateMenuReplaceOperationsWithNewMenuConfiguration { + for (NSOperation *operation in self.transactionQueue.operations) { + if ([operation isMemberOfClass:[SDLMenuReplaceOperation class]]) { + SDLMenuReplaceOperation *op = (SDLMenuReplaceOperation *)operation; + op.menuConfiguration = self.currentMenuConfiguration; } - - completionHandler(nil); - }]; -} - -#pragma mark Send New Menu Items - -/** - Creates add commands - - @param updatedMenu The cells you will be adding - @param menu The list of all cells. This may be different then self.menuCells since this function is called on subcell cells as well. When comparing 2 sub menu cells this function will be passed the list of all subcells on that cell. - @param completionHandler handler - */ -- (void)sdl_sendUpdatedMenu:(NSArray *)updatedMenu usingMenu:(NSArray *)menu withCompletionHandler:(SDLMenuUpdateCompletionHandler)completionHandler { - if (self.menuCells.count == 0 || updatedMenu.count == 0) { - SDLLogD(@"There are no cells to update."); - completionHandler(nil); - return; - } - - NSArray *mainMenuCommands = nil; - NSArray *subMenuCommands = nil; - - if (![self sdl_shouldRPCsIncludeImages:self.menuCells] || ![self.windowCapability hasImageFieldOfName:SDLImageFieldNameCommandIcon]) { - // Send artwork-less menu - mainMenuCommands = [self sdl_mainMenuCommandsForCells:updatedMenu withArtwork:NO usingIndexesFrom:menu]; - subMenuCommands = [self sdl_subMenuCommandsForCells:updatedMenu withArtwork:NO]; - } else { - // Send full artwork menu - mainMenuCommands = [self sdl_mainMenuCommandsForCells:updatedMenu withArtwork:YES usingIndexesFrom:menu]; - subMenuCommands = [self sdl_subMenuCommandsForCells:updatedMenu withArtwork:YES]; } - - self.inProgressUpdate = [mainMenuCommands arrayByAddingObjectsFromArray:subMenuCommands]; - - __block NSMutableDictionary *errors = [NSMutableDictionary dictionary]; - __weak typeof(self) weakSelf = self; - [self.connectionManager sendRequests:mainMenuCommands progressHandler:^void(__kindof SDLRPCRequest * _Nonnull request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error, float percentComplete) { - if (error != nil) { - errors[request] = error; - } - } completionHandler:^(BOOL success) { - if (!success) { - SDLLogE(@"Failed to send main menu commands: %@", errors); - completionHandler([NSError sdl_menuManager_failedToUpdateWithDictionary:errors]); - return; - } - - [weakSelf.connectionManager sendRequests:subMenuCommands progressHandler:^(__kindof SDLRPCRequest * _Nonnull request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error, float percentComplete) { - if (error != nil) { - errors[request] = error; - } - } completionHandler:^(BOOL success) { - if (!success) { - SDLLogE(@"Failed to send sub menu commands: %@", errors); - completionHandler([NSError sdl_menuManager_failedToUpdateWithDictionary:errors]); - return; - } - - SDLLogD(@"Finished updating menu"); - completionHandler(nil); - }]; - }]; } #pragma mark - Helpers @@ -532,96 +292,6 @@ - (BOOL)sdl_isDynamicMenuUpdateActive:(SDLDynamicMenuUpdatesMode)dynamicMenuUpda } } -- (NSArray *)sdl_removeUnusedProperties:(NSArray *)menuCells { - NSArray *removePropertiesCopy = [[NSArray alloc] initWithArray:menuCells copyItems:YES]; - for (SDLMenuCell *cell in removePropertiesCopy) { - // Strip away fields that cannot be used to determine uniqueness visually including fields not supported by the HMI - cell.voiceCommands = nil; - - // Don't check SDLImageFieldNameSubMenuIcon because it was added in 7.0 when the feature was added in 5.0. Just assume that if CommandIcon is not available, the submenu icon is not either. - if (![self.windowCapability hasImageFieldOfName:SDLImageFieldNameCommandIcon]) { - cell.icon = nil; - } - - if (cell.subCells != nil) { - if (![self.windowCapability hasTextFieldOfName:SDLTextFieldNameMenuSubMenuSecondaryText]) { - cell.secondaryText = nil; - } - if (![self.windowCapability hasTextFieldOfName:SDLTextFieldNameMenuSubMenuTertiaryText]) { - cell.tertiaryText = nil; - } - if (![self.windowCapability hasImageFieldOfName:SDLImageFieldNameMenuSubMenuSecondaryImage]) { - cell.secondaryArtwork = nil; - } - cell.subCells = [self sdl_removeUnusedProperties:cell.subCells]; - } else { - if (![self.windowCapability hasTextFieldOfName:SDLTextFieldNameMenuCommandSecondaryText]) { - cell.secondaryText = nil; - } - if (![self.windowCapability hasTextFieldOfName:SDLTextFieldNameMenuCommandTertiaryText]) { - cell.tertiaryText = nil; - } - if (![self.windowCapability hasImageFieldOfName:SDLImageFieldNameMenuCommandSecondaryImage]) { - cell.secondaryArtwork = nil; - } - } - } - - return removePropertiesCopy; -} - -/// 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_addUniqueNamesToCellsWithDuplicatePrimaryText:(nullable NSArray *)choices { - // Tracks how many of each cell primary text there are so that we can append numbers to make each unique as necessary - NSMutableDictionary *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_addUniqueNamesToCellsWithDuplicatePrimaryText:cell.subCells]; - } - } -} - -- (void)sdl_addUniqueNamesBasedOnStrippedCells:(NSArray *)strippedCells toUnstrippedCells:(NSArray *)unstrippedCells { - NSParameterAssert(strippedCells.count == unstrippedCells.count); - // Tracks how many of each cell primary text there are so that we can append numbers to make each unique as necessary - NSMutableDictionary *dictCounter = [[NSMutableDictionary alloc] init]; - for (NSUInteger i = 0; i < strippedCells.count; i++) { - SDLMenuCell *cell = strippedCells[i]; - NSNumber *counter = dictCounter[cell]; - if (counter != nil) { - counter = @(counter.intValue + 1); - dictCounter[cell] = counter; - } else { - dictCounter[cell] = @1; - } - - counter = dictCounter[cell]; - if (counter.intValue > 1) { - unstrippedCells[i].uniqueTitle = [NSString stringWithFormat: @"%@ (%d)", unstrippedCells[i].title, counter.intValue]; - } - - if (cell.subCells.count > 0) { - [self sdl_addUniqueNamesBasedOnStrippedCells:cell.subCells toUnstrippedCells:unstrippedCells[i].subCells]; - } - } -} - /// Check for cell lists with completely duplicate information, or any duplicate voiceCommands /// /// @param cells The cells you will be adding @@ -658,211 +328,6 @@ - (BOOL)sdl_menuCellsAreUnique:(NSArray *)cells allVoiceCommands: return YES; } -#pragma mark Artworks - -/// Get an array of artwork that needs to be uploaded form a list of menu cells -/// @param cells The menu cells to get artwork from -/// @returns The array of artwork that needs to be uploaded -- (NSArray *)sdl_findAllArtworksToBeUploadedFromCells:(NSArray *)cells { - if (![self.windowCapability hasImageFieldOfName:SDLImageFieldNameCommandIcon]) { - return @[]; - } - - NSMutableSet *mutableArtworks = [NSMutableSet set]; - for (SDLMenuCell *cell in cells) { - if ([self.fileManager fileNeedsUpload:cell.icon]) { - [mutableArtworks addObject:cell.icon]; - } - - if (cell.subCells.count > 0 && [self.windowCapability hasImageFieldOfName:SDLImageFieldNameMenuSubMenuSecondaryImage]) { - if ([self.fileManager fileNeedsUpload:cell.secondaryArtwork]) { - [mutableArtworks addObject:cell.secondaryArtwork]; - } - } else if (cell.subCells.count == 0 && [self.windowCapability hasImageFieldOfName:SDLImageFieldNameMenuCommandSecondaryImage]) { - if ([self.fileManager fileNeedsUpload:cell.secondaryArtwork]) { - [mutableArtworks addObject:cell.secondaryArtwork]; - } - } - - if (cell.subCells.count > 0) { - [mutableArtworks addObjectsFromArray:[self sdl_findAllArtworksToBeUploadedFromCells:cell.subCells]]; - } - } - - return [mutableArtworks allObjects]; -} - -/// Determine if cells should or should not be uploaded to the head unit with artworks. -/// -/// No artworks will be uploaded if: -/// -/// 1. If any cell has a dynamic artwork that is not uploaded -/// 2. If any cell contains a secondary artwork may be used on the head unit, and the cell has a dynamic secondary artwork that is not uploaded -/// 3. If any cell's subcells fails check (1) or (2) -/// @param cells The cells to check -/// @return True if the cells should be uploaded with artwork, false if they should not -- (BOOL)sdl_shouldRPCsIncludeImages:(NSArray *)cells { - for (SDLMenuCell *cell in cells) { - SDLArtwork *artwork = cell.icon; - SDLArtwork *secondaryArtwork = cell.secondaryArtwork; - if (artwork != nil && !artwork.isStaticIcon && ![self.fileManager hasUploadedFile:artwork]) { - return NO; - } else if (cell.subCells.count > 0 && [self.windowCapability hasImageFieldOfName:SDLImageFieldNameMenuSubMenuSecondaryImage]) { - if (secondaryArtwork != nil && !secondaryArtwork.isStaticIcon && ![self.fileManager hasUploadedFile:secondaryArtwork]) { - return NO; - } - } else if (cell.subCells.count == 0 && [self.windowCapability hasImageFieldOfName:SDLImageFieldNameMenuCommandSecondaryImage]) { - if (secondaryArtwork != nil && !secondaryArtwork.isStaticIcon && ![self.fileManager hasUploadedFile:secondaryArtwork]) { - return NO; - } - } else if (cell.subCells.count > 0 && ![self sdl_shouldRPCsIncludeImages:cell.subCells]) { - return NO; - } - } - - return YES; -} - -#pragma mark IDs - -/// Assign cell ids on an array of menu cells given a parent id (or no parent id) -/// @param menuCells The array of menu cells to update -/// @param parentId The parent id to assign if needed -- (void)sdl_updateIdsOnMenuCells:(NSArray *)menuCells parentId:(UInt32)parentId { - for (SDLMenuCell *cell in menuCells) { - cell.cellId = self.lastMenuId++; - cell.parentCellId = parentId; - if (cell.subCells.count > 0) { - [self sdl_updateIdsOnMenuCells:cell.subCells parentId:cell.cellId]; - } - } -} - -#pragma mark Deletes - -/// Create an array of DeleteCommand and DeleteSubMenu RPCs from an array of menu cells -/// @param cells The array of menu cells to use -- (NSArray *)sdl_deleteCommandsForCells:(NSArray *)cells { - NSMutableArray *mutableDeletes = [NSMutableArray array]; - for (SDLMenuCell *cell in cells) { - if (cell.subCells == nil) { - SDLDeleteCommand *delete = [[SDLDeleteCommand alloc] initWithId:cell.cellId]; - [mutableDeletes addObject:delete]; - } else { - SDLDeleteSubMenu *delete = [[SDLDeleteSubMenu alloc] initWithId:cell.cellId]; - [mutableDeletes addObject:delete]; - } - } - - return [mutableDeletes copy]; -} - -#pragma mark Commands / SubMenu RPCs - -/// This method will receive the cells to be added and the updated menu array. It will then build an array of add commands using the correct index to position the new items in the correct location. -/// e.g. If the new menu array is [A, B, C, D] but only [C, D] are new we need to pass [A, B , C , D] so C and D can be added to index 2 and 3 respectively. -/// -/// @param cells that will be added to the menu, this array must contain only cells that are not already in the menu. -/// @param shouldHaveArtwork artwork bool -/// @param menu the new menu array, this array should contain all the values the developer has set to be included in the new menu. This is used for placing the newly added cells in the correct location. -/// @return list of SDLRPCRequest addCommands -- (NSArray *)sdl_mainMenuCommandsForCells:(NSArray *)cells withArtwork:(BOOL)shouldHaveArtwork usingIndexesFrom:(NSArray *)menu { - NSMutableArray *mutableCommands = [NSMutableArray array]; - - for (NSUInteger menuInteger = 0; menuInteger < menu.count; menuInteger++) { - for (NSUInteger updateCellsIndex = 0; updateCellsIndex < cells.count; updateCellsIndex++) { - if ([menu[menuInteger] isEqual:cells[updateCellsIndex]]) { - if (cells[updateCellsIndex].subCells.count > 0) { - [mutableCommands addObject:[self sdl_subMenuCommandForMenuCell:cells[updateCellsIndex] withArtwork:shouldHaveArtwork position:(UInt16)menuInteger]]; - } else { - [mutableCommands addObject:[self sdl_commandForMenuCell:cells[updateCellsIndex] withArtwork:shouldHaveArtwork position:(UInt16)menuInteger]]; - } - } - } - } - - return [mutableCommands copy]; -} - -/// Creates SDLAddSubMenu RPCs for the passed array of menu cells, AND all of those cells' subcell RPCs, both SDLAddCommands and SDLAddSubMenus -/// @param cells The cells to create RPCs for -/// @param shouldHaveArtwork Whether artwork should be applied to the RPCs -/// @returns An array of RPCs of SDLAddSubMenus and their associated subcell RPCs -- (NSArray *)sdl_subMenuCommandsForCells:(NSArray *)cells withArtwork:(BOOL)shouldHaveArtwork { - NSMutableArray *mutableCommands = [NSMutableArray array]; - for (SDLMenuCell *cell in cells) { - if (cell.subCells.count > 0) { - [mutableCommands addObjectsFromArray:[self sdl_allCommandsForCells:cell.subCells withArtwork:shouldHaveArtwork]]; - } - } - - return [mutableCommands copy]; -} - -/// Creates SDLAddCommand and SDLAddSubMenu RPCs for a passed array of cells, AND all of those cells' subcell RPCs, both SDLAddCommands and SDLAddSubmenus -/// @param cells The cells to create RPCs for -/// @param shouldHaveArtwork Whether artwork should be applied to the RPCs -/// @returns An array of RPCs of SDLAddCommand and SDLAddSubMenus for the array of menu cells and their subcells, recursively -- (NSArray *)sdl_allCommandsForCells:(NSArray *)cells withArtwork:(BOOL)shouldHaveArtwork { - NSMutableArray *mutableCommands = [NSMutableArray array]; - - for (NSUInteger cellIndex = 0; cellIndex < cells.count; cellIndex++) { - if (cells[cellIndex].subCells.count > 0) { - [mutableCommands addObject:[self sdl_subMenuCommandForMenuCell:cells[cellIndex] withArtwork:shouldHaveArtwork position:(UInt16)cellIndex]]; - [mutableCommands addObjectsFromArray:[self sdl_allCommandsForCells:cells[cellIndex].subCells withArtwork:shouldHaveArtwork]]; - } else { - [mutableCommands addObject:[self sdl_commandForMenuCell:cells[cellIndex] withArtwork:shouldHaveArtwork position:(UInt16)cellIndex]]; - } - } - - return [mutableCommands copy]; -} - -/// An individual SDLAddCommand RPC for a given SDLMenuCell -/// @param cell The cell to create the RPC for -/// @param shouldHaveArtwork Whether artwork should be applied to the RPC -/// @param position The position the SDLAddCommand RPC should be given -/// @returns The SDLAddCommand RPC -- (SDLAddCommand *)sdl_commandForMenuCell:(SDLMenuCell *)cell withArtwork:(BOOL)shouldHaveArtwork position:(UInt16)position { - SDLAddCommand *command = [[SDLAddCommand alloc] init]; - - SDLMenuParams *params = [[SDLMenuParams alloc] init]; - params.menuName = cell.uniqueTitle; - params.parentID = cell.parentCellId != UINT32_MAX ? @(cell.parentCellId) : nil; - params.position = @(position); - params.secondaryText = (cell.secondaryText.length > 0 && [self.windowCapability hasTextFieldOfName:SDLTextFieldNameMenuCommandSecondaryText]) ? cell.secondaryText : nil; - params.tertiaryText = (cell.tertiaryText.length > 0 && [self.windowCapability hasTextFieldOfName:SDLTextFieldNameMenuCommandTertiaryText]) ? cell.tertiaryText : nil; - - command.menuParams = params; - command.vrCommands = (cell.voiceCommands.count == 0) ? nil : cell.voiceCommands; - command.cmdIcon = (cell.icon && shouldHaveArtwork) ? cell.icon.imageRPC : nil; - command.cmdID = @(cell.cellId); - command.secondaryImage = (cell.secondaryArtwork && shouldHaveArtwork && ![self.fileManager fileNeedsUpload:cell.secondaryArtwork] && [self.windowCapability hasImageFieldOfName:SDLImageFieldNameMenuCommandSecondaryImage]) ? cell.secondaryArtwork.imageRPC : nil; - - return command; -} - -/// An individual SDLAddSubMenu RPC for a given SDLMenuCell -/// @param cell The cell to create the RPC for -/// @param shouldHaveArtwork Whether artwork should be applied to the RPC -/// @param position The position the SDLAddSubMenu RPC should be given -/// @returns The SDLAddSubMenu RPC -- (SDLAddSubMenu *)sdl_subMenuCommandForMenuCell:(SDLMenuCell *)cell withArtwork:(BOOL)shouldHaveArtwork position:(UInt16)position { - SDLImage *icon = (shouldHaveArtwork && (cell.icon.name != nil)) ? cell.icon.imageRPC : nil; - SDLImage *secondaryImage = (shouldHaveArtwork && ![self.fileManager fileNeedsUpload:cell.secondaryArtwork] && (cell.secondaryArtwork.name != nil) && [self.windowCapability hasImageFieldOfName:SDLImageFieldNameMenuSubMenuSecondaryImage]) ? cell.secondaryArtwork.imageRPC : nil; - - SDLMenuLayout submenuLayout = nil; - if (cell.submenuLayout && [self.systemCapabilityManager.defaultMainWindowCapability.menuLayoutsAvailable containsObject:cell.submenuLayout]) { - submenuLayout = cell.submenuLayout; - } else { - submenuLayout = self.menuConfiguration.defaultSubmenuLayout; - } - - NSString *secondaryText = (cell.secondaryText.length > 0 && [self.windowCapability hasTextFieldOfName:SDLTextFieldNameMenuSubMenuSecondaryText]) ? cell.secondaryText : nil; - NSString *tertiaryText = (cell.tertiaryText.length > 0 && [self.windowCapability hasTextFieldOfName:SDLTextFieldNameMenuSubMenuTertiaryText]) ? cell.tertiaryText : nil; - 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 /// Call a handler for a currently displayed SDLMenuCell based on the incoming SDLOnCommand notification @@ -889,46 +354,22 @@ - (BOOL)sdl_callHandlerForCells:(NSArray *)cells command:(SDLOnCo - (void)sdl_commandNotification:(SDLRPCNotificationNotification *)notification { SDLOnCommand *onCommand = (SDLOnCommand *)notification.notification; - - [self sdl_callHandlerForCells:self.menuCells command:onCommand]; + [self sdl_callHandlerForCells:self.currentMenuCells command:onCommand]; } - (void)sdl_displayCapabilityDidUpdate { self.windowCapability = self.systemCapabilityManager.defaultMainWindowCapability; + [self sdl_updateMenuReplaceOperationsWithNewWindowCapability]; } - (void)sdl_hmiStatusNotification:(SDLRPCNotificationNotification *)notification { SDLOnHMIStatus *hmiStatus = (SDLOnHMIStatus *)notification.notification; + if ((hmiStatus.windowID != nil) && (hmiStatus.windowID.integerValue != SDLPredefinedWindowsDefaultWindow)) { return; } - if (hmiStatus.windowID != nil && hmiStatus.windowID.integerValue != SDLPredefinedWindowsDefaultWindow) { - return; - } - - SDLHMILevel oldHMILevel = self.currentHMILevel; self.currentHMILevel = hmiStatus.hmiLevel; - - // Auto-send an updated menu if we were in NONE and now we are not, and we need an update - if ([oldHMILevel isEqualToString:SDLHMILevelNone] && ![self.currentHMILevel isEqualToString:SDLHMILevelNone] && - ![self.currentSystemContext isEqualToEnum:SDLSystemContextMenu]) { - if (self.waitingOnHMIUpdate) { - [self setMenuCells:self.waitingUpdateMenuCells]; - self.waitingUpdateMenuCells = @[]; - return; - } - } - - // If we don't check for this and only update when not in the menu, there can be IN_USE errors, especially with submenus. We also don't want to encourage changing out the menu while the user is using it for usability reasons. - SDLSystemContext oldSystemContext = self.currentSystemContext; self.currentSystemContext = hmiStatus.systemContext; - if ([oldSystemContext isEqualToEnum:SDLSystemContextMenu] - && ![self.currentSystemContext isEqualToEnum:SDLSystemContextMenu] - && ![self.currentHMILevel isEqualToEnum:SDLHMILevelNone]) { - if (self.waitingOnHMIUpdate) { - [self setMenuCells:self.waitingUpdateMenuCells]; - self.waitingUpdateMenuCells = @[]; - } - } + [self sdl_updateTransactionQueueSuspended]; } @end diff --git a/SmartDeviceLink/private/SDLMenuManagerPrivateConstants.h b/SmartDeviceLink/private/SDLMenuManagerPrivateConstants.h new file mode 100644 index 000000000..63f7333ee --- /dev/null +++ b/SmartDeviceLink/private/SDLMenuManagerPrivateConstants.h @@ -0,0 +1,15 @@ +// +// SDLMenuManagerPrivateConstants.h +// SmartDeviceLink +// +// Created by Joel Fischer on 1/27/21. +// Copyright © 2021 smartdevicelink. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern UInt32 const ParentIdNotFound; + +NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLink/private/SDLMenuManagerPrivateConstants.m b/SmartDeviceLink/private/SDLMenuManagerPrivateConstants.m new file mode 100644 index 000000000..03c7349e8 --- /dev/null +++ b/SmartDeviceLink/private/SDLMenuManagerPrivateConstants.m @@ -0,0 +1,15 @@ +// +// SDLMenuManagerPrivateConstants.m +// SmartDeviceLink +// +// Created by Joel Fischer on 1/27/21. +// Copyright © 2021 smartdevicelink. All rights reserved. +// + +#import "SDLMenuManagerPrivateConstants.h" + +NS_ASSUME_NONNULL_BEGIN + +UInt32 const ParentIdNotFound = UINT32_MAX; + +NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLink/private/SDLMenuReplaceOperation.h b/SmartDeviceLink/private/SDLMenuReplaceOperation.h new file mode 100644 index 000000000..05a7d25f6 --- /dev/null +++ b/SmartDeviceLink/private/SDLMenuReplaceOperation.h @@ -0,0 +1,36 @@ +// +// SDLMenuReplaceOperation.h +// SmartDeviceLink +// +// Created by Joel Fischer on 1/20/21. +// Copyright © 2021 smartdevicelink. All rights reserved. +// + +#import "SDLAsynchronousOperation.h" + +#import "SDLAsynchronousOperation.h" + +#import "SDLMenuReplaceUtilities.h" + +@protocol SDLConnectionManagerType; + +@class SDLFileManager; +@class SDLMenuCell; +@class SDLMenuConfiguration; +@class SDLWindowCapability; + +NS_ASSUME_NONNULL_BEGIN + +typedef void(^SDLCurrentMenuUpdatedBlock)(NSArray *currentMenuCells, NSError *_Nullable error); + +@interface SDLMenuReplaceOperation : SDLAsynchronousOperation + +@property (strong, nonatomic) SDLWindowCapability *windowCapability; +@property (strong, nonatomic) SDLMenuConfiguration *menuConfiguration; +@property (strong, nonatomic) NSArray *currentMenu; + +- (instancetype)initWithConnectionManager:(id)connectionManager fileManager:(SDLFileManager *)fileManager windowCapability:(SDLWindowCapability *)windowCapability menuConfiguration:(SDLMenuConfiguration *)menuConfiguration currentMenu:(NSArray *)currentMenu updatedMenu:(NSArray *)updatedMenu compatibilityModeEnabled:(BOOL)compatibilityModeEnabled currentMenuUpdatedHandler:(SDLCurrentMenuUpdatedBlock)currentMenuUpdatedHandler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLink/private/SDLMenuReplaceOperation.m b/SmartDeviceLink/private/SDLMenuReplaceOperation.m new file mode 100644 index 000000000..8f656c8a5 --- /dev/null +++ b/SmartDeviceLink/private/SDLMenuReplaceOperation.m @@ -0,0 +1,455 @@ +// +// SDLMenuReplaceDynamicOperation.m +// SmartDeviceLink +// +// Created by Joel Fischer on 1/20/21. +// Copyright © 2021 smartdevicelink. All rights reserved. +// + +#import "SDLMenuReplaceOperation.h" + +#import "SDLArtwork.h" +#import "SDLConnectionManagerType.h" +#import "SDLDynamicMenuUpdateAlgorithm.h" +#import "SDLDynamicMenuUpdateRunScore.h" +#import "SDLError.h" +#import "SDLFileManager.h" +#import "SDLGlobals.h" +#import "SDLLogMacros.h" +#import "SDLMenuCell.h" +#import "SDLMenuConfiguration.h" +#import "SDLMenuManagerPrivateConstants.h" +#import "SDLTextFieldName.h" +#import "SDLVersion.h" +#import "SDLWindowCapability.h" +#import "SDLWindowCapability+ScreenManagerExtensions.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface SDLMenuCell() + +@property (assign, nonatomic) UInt32 parentCellId; +@property (assign, nonatomic) UInt32 cellId; +@property (strong, nonatomic, readwrite) NSString *uniqueTitle; + +@property (copy, nonatomic, readwrite) NSString *title; +@property (strong, nonatomic, readwrite, nullable) SDLArtwork *icon; +@property (copy, nonatomic, readwrite, nullable) NSArray *voiceCommands; +@property (copy, nonatomic, readwrite, nullable) NSString *secondaryText; +@property (copy, nonatomic, readwrite, nullable) NSString *tertiaryText; +@property (strong, nonatomic, readwrite, nullable) SDLArtwork *secondaryArtwork; +@property (copy, nonatomic, readwrite, nullable) NSArray *subCells; +@property (copy, nonatomic, readwrite, nullable) SDLMenuCellSelectionHandler handler; + +@end + +@interface SDLMenuReplaceOperation () + +// Dependencies +@property (weak, nonatomic) id connectionManager; +@property (weak, nonatomic) SDLFileManager *fileManager; +@property (strong, nonatomic) NSArray *updatedMenu; +@property (strong, nonatomic) NSMutableArray *mutableCurrentMenu; +@property (assign, nonatomic) BOOL compatibilityModeEnabled; +@property (copy, nonatomic) SDLCurrentMenuUpdatedBlock currentMenuUpdatedHandler; + +// Internal properties +@property (copy, nonatomic, nullable) NSError *internalError; + +@end + +@implementation SDLMenuReplaceOperation + +- (instancetype)initWithConnectionManager:(id)connectionManager fileManager:(SDLFileManager *)fileManager windowCapability:(SDLWindowCapability *)windowCapability menuConfiguration:(SDLMenuConfiguration *)menuConfiguration currentMenu:(NSArray *)currentMenu updatedMenu:(NSArray *)updatedMenu compatibilityModeEnabled:(BOOL)compatibilityModeEnabled currentMenuUpdatedHandler:(SDLCurrentMenuUpdatedBlock)currentMenuUpdatedHandler { + self = [super init]; + if (!self) { return nil; } + + _connectionManager = connectionManager; + _fileManager = fileManager; + _windowCapability = windowCapability; + _menuConfiguration = menuConfiguration; + _mutableCurrentMenu = [currentMenu mutableCopy]; + _updatedMenu = [updatedMenu copy]; + _compatibilityModeEnabled = compatibilityModeEnabled; + _currentMenuUpdatedHandler = currentMenuUpdatedHandler; + + return self; +} + +- (void)start { + [super start]; + if (self.isCancelled) { return; } + + [SDLMenuReplaceUtilities addIdsToMenuCells:self.updatedMenu parentId:ParentIdNotFound]; + + // Strip the "current menu" and the new menu of properties that are not displayed on the head unit + NSArray *updatedStrippedMenu = [self.class sdl_cellsWithRemovedPropertiesFromCells:self.updatedMenu basedOnWindowCapability:self.windowCapability]; + NSArray *currentStrippedMenu = [self.class sdl_cellsWithRemovedPropertiesFromCells:self.mutableCurrentMenu basedOnWindowCapability:self.windowCapability]; + + // Generate unique names and ensure that all menus we are tracking have them so that we can properly compare when using the dynamic algorithm + BOOL supportsMenuUniqueness = [[SDLGlobals sharedGlobals].rpcVersion isGreaterThanOrEqualToVersion:[SDLVersion versionWithMajor:7 minor:1 patch:0]]; + [self.class sdl_generateUniqueNamesForCells:updatedStrippedMenu supportsMenuUniqueness:supportsMenuUniqueness]; + [self.class sdl_applyUniqueNamesOnCells:updatedStrippedMenu toCells:self.updatedMenu]; + + SDLDynamicMenuUpdateRunScore *runScore = nil; + if (self.compatibilityModeEnabled) { + SDLLogV(@"Dynamic menu update inactive. Forcing the deletion of all old cells and adding all new ones, even if they're the same."); + runScore = [SDLDynamicMenuUpdateAlgorithm compatibilityRunScoreWithOldMenuCells:currentStrippedMenu updatedMenuCells:updatedStrippedMenu]; + } else { + SDLLogV(@"Dynamic menu update active. Running the algorithm to find the best way to delete / add cells."); + runScore = [SDLDynamicMenuUpdateAlgorithm dynamicRunScoreOldMenuCells:currentStrippedMenu updatedMenuCells:updatedStrippedMenu]; + } + + // If both old and new cells are empty, nothing needs to happen + if (runScore.isEmpty) { return [self finishOperation]; } + + // Drop the cells into buckets based on the run score + NSArray *cellsToDelete = [self sdl_filterDeleteMenuItemsWithOldMenuItems:self.currentMenu basedOnStatusList:runScore.oldStatus]; + NSArray *cellsToAdd = [self sdl_filterAddMenuItemsWithNewMenuItems:self.updatedMenu basedOnStatusList:runScore.updatedStatus]; + // These arrays should ONLY contain KEEPS. These will be used for SubMenu compares + NSArray *oldKeeps = [self sdl_filterKeepMenuItems:self.currentMenu basedOnStatusList:runScore.oldStatus]; + NSArray *newKeeps = [self sdl_filterKeepMenuItems:self.updatedMenu basedOnStatusList:runScore.updatedStatus]; + + // Old kept cells ids need to be moved to the new kept cells so that submenu changes have correct parent ids + [SDLMenuReplaceUtilities transferCellIDsFromCells:oldKeeps toCells:newKeeps]; + + // Transfer new cells' handlers to the old cells, which are stored in the current menu + [SDLMenuReplaceUtilities transferCellHandlersFromCells:newKeeps toCells:oldKeeps]; + + // Upload the artworks, then we will start updating the main menu + __weak typeof(self) weakSelf = self; + [self sdl_uploadMenuArtworksWithCompletionHandler:^(NSError * _Nullable error) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (strongSelf.isCancelled) { return [strongSelf finishOperation]; } + if (error != nil) { return [strongSelf finishOperationWithError:error]; } + + [strongSelf sdl_updateMenuWithCellsToDelete:cellsToDelete cellsToAdd:cellsToAdd completionHandler:^(NSError * _Nullable error) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (strongSelf.isCancelled) { return [strongSelf finishOperation]; } + if (error != nil) { return [strongSelf finishOperationWithError:error]; } + + [strongSelf sdl_updateSubMenuWithOldKeptCells:oldKeeps newKeptCells:newKeeps keptCellIndex:0 completionHandler:^(NSError * _Nullable error) { + return [strongSelf finishOperationWithError:error]; + }]; + }]; + }]; +} + +#pragma mark - Update Main Menu / Submenu + +- (void)sdl_uploadMenuArtworksWithCompletionHandler:(void(^)(NSError *_Nullable error))handler { + NSArray *artworksToBeUploaded = [SDLMenuReplaceUtilities findAllArtworksToBeUploadedFromCells:self.updatedMenu fileManager:self.fileManager windowCapability:self.windowCapability]; + if (artworksToBeUploaded.count == 0) { return handler(nil); } + + __weak typeof(self) weakself = self; + [self.fileManager uploadArtworks:artworksToBeUploaded progressHandler:^BOOL(NSString * _Nonnull artworkName, float uploadPercentage, NSError * _Nullable error) { + // If we're cancelled, stop uploading + return !weakself.isCancelled; + } completionHandler:^(NSArray * _Nonnull artworkNames, NSError * _Nullable error) { + if (error != nil) { SDLLogE(@"Error uploading menu artworks: %@", error); } + + SDLLogD(@"Menu artwork upload completed, beginning upload of main menu"); + // Start updating the main menu cells + handler(error); + }]; +} + +/// Takes the main menu cells to delete and add, and deletes the current menu cells, then adds the new menu cells in the correct locations +/// @param deleteCells The cells that need to be deleted +/// @param addCells The cells that need to be added +/// @param handler A handler called when complete +- (void)sdl_updateMenuWithCellsToDelete:(NSArray *)deleteCells cellsToAdd:(NSArray *)addCells completionHandler:(void(^)(NSError *_Nullable error))handler { + __weak typeof(self) weakSelf = self; + [self sdl_sendDeleteMenuCells:deleteCells withCompletionHandler:^(NSError * _Nullable error) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (strongSelf.isCancelled) { return handler(error); } + [strongSelf sdl_sendAddMenuCells:addCells withPositionsFromMenu:self.updatedMenu completionHandler:^(NSError * _Nullable error) { + handler(error); + }]; + }]; +} + +/// Takes the submenu cells that are old keeps and new keeps and determines which cells need to be deleted or added +/// @param oldKeptCells The old kept cells +/// @param newKeptCells The new kept cells +/// @param index The index of the main menu to use +/// @param completionHandler The handler to call when all submenu updates are complete +- (void)sdl_updateSubMenuWithOldKeptCells:(NSArray *)oldKeptCells newKeptCells:(NSArray *)newKeptCells keptCellIndex:(NSUInteger)index completionHandler:(void(^)(NSError *_Nullable error))completionHandler { + if (oldKeptCells.count == 0 || index >= oldKeptCells.count) { return completionHandler(nil); } + + if (oldKeptCells[index].subCells.count > 0) { + SDLDynamicMenuUpdateRunScore *tempScore = [SDLDynamicMenuUpdateAlgorithm dynamicRunScoreOldMenuCells:oldKeptCells[index].subCells updatedMenuCells:newKeptCells[index].subCells]; + NSArray *deleteMenuStatus = tempScore.oldStatus; + NSArray *addMenuStatus = tempScore.updatedStatus; + + NSArray *cellsToDelete = [self sdl_filterDeleteMenuItemsWithOldMenuItems:oldKeptCells[index].subCells basedOnStatusList:deleteMenuStatus]; + NSArray *cellsToAdd = [self sdl_filterAddMenuItemsWithNewMenuItems:newKeptCells[index].subCells basedOnStatusList:addMenuStatus]; + + // Transfer ids from subcell keeps to old subcells, which are stored in the current menu + NSArray *oldSubcellKeeps = [self sdl_filterKeepMenuItems:oldKeptCells[index].subCells basedOnStatusList:deleteMenuStatus]; + NSArray *newSubcellKeeps = [self sdl_filterKeepMenuItems:newKeptCells[index].subCells basedOnStatusList:addMenuStatus]; + [SDLMenuReplaceUtilities transferCellHandlersFromCells:newSubcellKeeps toCells:oldSubcellKeeps]; + + __weak typeof(self) weakself = self; + [self sdl_sendDeleteMenuCells:cellsToDelete withCompletionHandler:^(NSError * _Nullable error) { + if (weakself.isCancelled) { return completionHandler([NSError sdl_menuManager_replaceOperationCancelled]); } + if (error != nil) { return completionHandler(error); } + + [weakself sdl_sendAddMenuCells:cellsToAdd withPositionsFromMenu:newKeptCells[index].subCells completionHandler:^(NSError * _Nullable error) { + if (weakself.isCancelled) { return completionHandler([NSError sdl_menuManager_replaceOperationCancelled]); } + if (error != nil) { return completionHandler(error); } + + // After the first set of submenu cells were added and deleted we must find the next set of subcells until we loop through all the elements + [weakself sdl_updateSubMenuWithOldKeptCells:oldKeptCells newKeptCells:newKeptCells keptCellIndex:(index + 1) completionHandler:^(NSError * _Nullable error) { + completionHandler(error); + }]; + }]; + }]; + } else { + // There are no subcells, we can skip to the next index. + [self sdl_updateSubMenuWithOldKeptCells:oldKeptCells newKeptCells:newKeptCells keptCellIndex:(index + 1) completionHandler:^(NSError * _Nullable error) { + completionHandler(error); + }]; + } +} + +#pragma mark - Adding / Deleting Cell RPCs + +/// Send Delete RPCs for given menu cells +/// @param deleteMenuCells The menu cells to be deleted +/// @param completionHandler A handler called when the RPCs are finished with an error if any failed +- (void)sdl_sendDeleteMenuCells:(nullable NSArray *)deleteMenuCells withCompletionHandler:(void(^)(NSError *_Nullable error))completionHandler { + if (deleteMenuCells.count == 0) { return completionHandler(nil); } + + __block NSMutableDictionary *errors = [NSMutableDictionary dictionary]; + NSArray *deleteMenuCommands = [SDLMenuReplaceUtilities deleteCommandsForCells:deleteMenuCells]; + [self.connectionManager sendRequests:deleteMenuCommands progressHandler:^void(__kindof SDLRPCRequest * _Nonnull request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error, float percentComplete) { + if (error != nil) { + errors[request] = error; + } else if (response.success.boolValue) { + // Find the id of the successful request and remove it from the current menu list wherever it may have been + UInt32 commandId = [SDLMenuReplaceUtilities commandIdForRPCRequest:request]; + [SDLMenuReplaceUtilities removeCellFromList:self.mutableCurrentMenu withCellId:commandId]; + } + } completionHandler:^(BOOL success) { + if (!success) { + SDLLogE(@"Unable to delete all old menu commands with errors: %@", errors); + completionHandler([NSError sdl_menuManager_failedToUpdateWithDictionary:errors]); + } else { + SDLLogD(@"Finished deleting old menu"); + completionHandler(nil); + } + }]; +} + +/// Send Add RPCs for given new menu cells compared to old menu cells +/// @param addMenuCells The new menu cells we want displayed +/// @param fullMenu The full menu from which the addMenuCells come. This allows us to grab the positions from that menu for the new cells +/// @param completionHandler A handler called when the RPCs are finished with an error if any failed +- (void)sdl_sendAddMenuCells:(NSArray *)addMenuCells withPositionsFromMenu:(NSArray *)fullMenu completionHandler:(void(^)(NSError *_Nullable error))completionHandler { + if (addMenuCells.count == 0) { + SDLLogV(@"There are no cells to update."); + return completionHandler(nil); + } + + NSArray *mainMenuCommands = [SDLMenuReplaceUtilities mainMenuCommandsForCells:addMenuCells fileManager:self.fileManager usingPositionsFromFullMenu:fullMenu windowCapability:self.windowCapability defaultSubmenuLayout:self.menuConfiguration.defaultSubmenuLayout]; + NSArray *subMenuCommands = [SDLMenuReplaceUtilities subMenuCommandsForCells:addMenuCells fileManager:self.fileManager windowCapability:self.windowCapability defaultSubmenuLayout:self.menuConfiguration.defaultSubmenuLayout]; + + __block NSMutableDictionary *errors = [NSMutableDictionary dictionary]; + __weak typeof(self) weakSelf = self; + [self.connectionManager sendRequests:mainMenuCommands progressHandler:^void(__kindof SDLRPCRequest * _Nonnull request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error, float percentComplete) { + if (error != nil) { + errors[request] = error; + } else { + // Find the id of the successful request and add it from the current menu list wherever it needs to be + UInt32 commandId = [SDLMenuReplaceUtilities commandIdForRPCRequest:request]; + UInt16 position = [SDLMenuReplaceUtilities positionForRPCRequest:request]; + [SDLMenuReplaceUtilities addCellWithCellId:commandId position:position fromNewMenuList:addMenuCells toMainMenuList:weakSelf.mutableCurrentMenu]; + } + } completionHandler:^(BOOL success) { + if (!success) { + SDLLogE(@"Failed to send main menu commands: %@", errors); + return completionHandler([NSError sdl_menuManager_failedToUpdateWithDictionary:errors]); + } else if (subMenuCommands.count == 0) { + SDLLogD(@"Finished sending new cells"); + return completionHandler(nil); + } + + [weakSelf.connectionManager sendRequests:subMenuCommands progressHandler:^(__kindof SDLRPCRequest * _Nonnull request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error, float percentComplete) { + if (error != nil) { + errors[request] = error; + } else { + // Find the id of the successful request and add it from the current menu list wherever it needs to be + UInt32 commandId = [SDLMenuReplaceUtilities commandIdForRPCRequest:request]; + UInt16 position = [SDLMenuReplaceUtilities positionForRPCRequest:request]; + [SDLMenuReplaceUtilities addCellWithCellId:commandId position:position fromNewMenuList:addMenuCells toMainMenuList:weakSelf.mutableCurrentMenu]; + } + } completionHandler:^(BOOL success) { + if (!success) { + SDLLogE(@"Failed to send sub menu commands: %@", errors); + return completionHandler([NSError sdl_menuManager_failedToUpdateWithDictionary:errors]); + } + + SDLLogD(@"Finished sending new cells"); + completionHandler(nil); + }]; + }]; +} + +#pragma mark - Dynamic Menu Helpers + +- (NSArray *)sdl_filterDeleteMenuItemsWithOldMenuItems:(NSArray *)oldMenuCells basedOnStatusList:(NSArray *)oldStatusList { + NSMutableArray *deleteCells = [[NSMutableArray alloc] init]; + // The index of the status should correlate 1-1 with the number of items in the menu + // [2,0,2,0] => [A,B,C,D] = [B,D] + for (NSUInteger index = 0; index < oldStatusList.count; index++) { + if (oldStatusList[index].integerValue == SDLMenuCellUpdateStateDelete) { + [deleteCells addObject:oldMenuCells[index]]; + } + } + return [deleteCells copy]; +} + +- (NSArray *)sdl_filterAddMenuItemsWithNewMenuItems:(NSArray *)newMenuCells basedOnStatusList:(NSArray *)newStatusList { + NSMutableArray *addCells = [[NSMutableArray alloc] init]; + // The index of the status should correlate 1-1 with the number of items in the menu + // [2,1,2,1] => [A,B,C,D] = [B,D] + for (NSUInteger index = 0; index < newStatusList.count; index++) { + if (newStatusList[index].integerValue == SDLMenuCellUpdateStateAdd) { + [addCells addObject:newMenuCells[index]]; + } + } + return [addCells copy]; +} + +- (NSArray *)sdl_filterKeepMenuItems:(NSArray *)menuCells basedOnStatusList:(NSArray *)keepStatusList { + NSMutableArray *keepMenuCells = [[NSMutableArray alloc] init]; + + for (NSUInteger i = 0; i < keepStatusList.count; i++) { + if (keepStatusList[i].unsignedIntegerValue == SDLMenuCellUpdateStateKeep) { + [keepMenuCells addObject:menuCells[i]]; + } + } + return [keepMenuCells copy]; +} + +#pragma mark - Menu Uniqueness + ++ (NSArray *)sdl_cellsWithRemovedPropertiesFromCells:(NSArray *)menuCells basedOnWindowCapability:(SDLWindowCapability *)windowCapability { + NSArray *removePropertiesCopy = [[NSArray alloc] initWithArray:menuCells copyItems:YES]; + for (SDLMenuCell *cell in removePropertiesCopy) { + // Strip away fields that cannot be used to determine uniqueness visually including fields not supported by the HMI + cell.voiceCommands = nil; + + // Don't check SDLImageFieldNameSubMenuIcon because it was added in 7.0 when the feature was added in 5.0. Just assume that if CommandIcon is not available, the submenu icon is not either. + if (![windowCapability hasImageFieldOfName:SDLImageFieldNameCommandIcon]) { + cell.icon = nil; + } + + if (cell.subCells != nil) { + if (![windowCapability hasTextFieldOfName:SDLTextFieldNameMenuSubMenuSecondaryText]) { + cell.secondaryText = nil; + } + if (![windowCapability hasTextFieldOfName:SDLTextFieldNameMenuSubMenuTertiaryText]) { + cell.tertiaryText = nil; + } + if (![windowCapability hasImageFieldOfName:SDLImageFieldNameMenuSubMenuSecondaryImage]) { + cell.secondaryArtwork = nil; + } + cell.subCells = [self sdl_cellsWithRemovedPropertiesFromCells:cell.subCells basedOnWindowCapability:windowCapability]; + } else { + if (![windowCapability hasTextFieldOfName:SDLTextFieldNameMenuCommandSecondaryText]) { + cell.secondaryText = nil; + } + if (![windowCapability hasTextFieldOfName:SDLTextFieldNameMenuCommandTertiaryText]) { + cell.tertiaryText = nil; + } + if (![windowCapability hasImageFieldOfName:SDLImageFieldNameMenuCommandSecondaryImage]) { + cell.secondaryArtwork = nil; + } + } + } + + return removePropertiesCopy; +} + ++ (void)sdl_generateUniqueNamesForCells:(NSArray *)menuCells supportsMenuUniqueness:(BOOL)supportsMenuUniqueness { + // Tracks how many of each cell primary text there are so that we can append numbers to make each unique as necessary + NSMutableDictionary, NSNumber *> *dictCounter = [[NSMutableDictionary alloc] init]; + for (NSUInteger i = 0; i < menuCells.count; i++) { + id key = supportsMenuUniqueness ? menuCells[i] : menuCells[i].title; + NSNumber *counter = dictCounter[key]; + if (counter != nil) { + counter = @(counter.intValue + 1); + dictCounter[key] = counter; + } else { + dictCounter[key] = @1; + } + + counter = dictCounter[key]; + if (counter.intValue > 1) { + menuCells[i].uniqueTitle = [NSString stringWithFormat: @"%@ (%d)", menuCells[i].title, counter.intValue]; + } + + if (menuCells[i].subCells.count > 0) { + [self sdl_generateUniqueNamesForCells:menuCells[i].subCells supportsMenuUniqueness:supportsMenuUniqueness]; + } + } +} + ++ (void)sdl_applyUniqueNamesOnCells:(NSArray *)fromMenuCells toCells:(NSArray *)toMenuCells { + NSParameterAssert(fromMenuCells.count == toMenuCells.count); + + for (NSUInteger i = 0; i < fromMenuCells.count; i++) { + toMenuCells[i].uniqueTitle = fromMenuCells[i].uniqueTitle; + if (fromMenuCells[i].subCells.count > 0) { + [self sdl_applyUniqueNamesOnCells:fromMenuCells[i].subCells toCells:toMenuCells[i].subCells]; + } + } +} + +#pragma mark - Getter / Setters + +- (void)setCurrentMenu:(NSArray *)currentMenu { + _mutableCurrentMenu = [currentMenu mutableCopy]; +} + +- (NSArray *)currentMenu { + return [_mutableCurrentMenu copy]; +} + +#pragma mark - Operation Overrides + +- (void)finishOperationWithError:(nullable NSError *)error { + if (error != nil) { + self.internalError = error; + } + + [self finishOperation]; +} + +- (void)finishOperation { + SDLLogV(@"Finishing menu manager replace operation"); + if (self.isCancelled) { + self.internalError = [NSError sdl_menuManager_replaceOperationCancelled]; + } + + self.currentMenuUpdatedHandler(self.currentMenu, self.error); + [super finishOperation]; +} + +- (nullable NSString *)name { + return @"com.sdl.menuManager.replaceMenu.dynamic"; +} + +- (NSOperationQueuePriority)queuePriority { + return NSOperationQueuePriorityNormal; +} + +- (nullable NSError *)error { + return self.internalError; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLink/private/SDLMenuReplaceUtilities.h b/SmartDeviceLink/private/SDLMenuReplaceUtilities.h new file mode 100644 index 000000000..4488d0d4b --- /dev/null +++ b/SmartDeviceLink/private/SDLMenuReplaceUtilities.h @@ -0,0 +1,87 @@ +// +// SDLMenuReplaceUtilities.h +// SmartDeviceLink +// +// Created by Joel Fischer on 1/22/21. +// Copyright © 2021 smartdevicelink. All rights reserved. +// + +#import + +#import "SDLMenuLayout.h" + +@class SDLArtwork; +@class SDLFileManager; +@class SDLMenuCell; +@class SDLMenuConfiguration; +@class SDLRPCRequest; +@class SDLWindowCapability; + +NS_ASSUME_NONNULL_BEGIN + +@interface SDLMenuReplaceUtilities : NSObject + +#pragma mark - Ids + ++ (void)addIdsToMenuCells:(NSArray *)menuCells parentId:(UInt32)parentId; + ++ (void)transferCellIDsFromCells:(NSArray *)fromCells toCells:(NSArray *)toCells; + ++ (void)transferCellHandlersFromCells:(NSArray *)fromCells toCells:(NSArray *)toCells; + +#pragma mark - Artworks + +/// Finds all artworks that need to be uploaded from the given list of menu cells +/// @param cells The cells to check for artwork +/// @param fileManager The file manager to check if artworks need upload +/// @param windowCapability The window capability to check available image fields ++ (NSArray *)findAllArtworksToBeUploadedFromCells:(NSArray *)cells fileManager:(SDLFileManager *)fileManager windowCapability:(SDLWindowCapability *)windowCapability; + +#pragma mark - RPC Commands + +/// Finds and returns the command id for a given RPC request, assuming that request is an SDLDeleteSubMenu, SDLDeleteCommand, SDLAddSubMenu, or SDLAddCommand +/// @param request The request ++ (UInt32)commandIdForRPCRequest:(SDLRPCRequest *)request; + +/// Finds and returns the position for a given RPC request, assuming that request is an SDLAddSubMenu, or SDLAddCommand +/// @param request The request ++ (UInt16)positionForRPCRequest:(SDLRPCRequest *)request; + +/// Generate SDLDeleteCommand and SDLDeleteSubMenu RPCs for the given cells +/// @param cells The cells for which to generate delete RPCs ++ (NSArray *)deleteCommandsForCells:(NSArray *)cells; + +/// Generate SDLAddCommand and SDLAddSubMenu RPCs for given main menu cells +/// @param cells The cells to generate AddCommand / AddSubMenu RPCs for +/// @param fileManager The file manager to use to check availability of artworks +/// @param menu The menu from which we will manage indexes +/// @param windowCapability The window capability with which to check available text fields / image fields +/// @param defaultSubmenuLayout The default submenu layout to use for displaying submenus ++ (NSArray *)mainMenuCommandsForCells:(NSArray *)cells fileManager:(SDLFileManager *)fileManager usingPositionsFromFullMenu:(NSArray *)menu windowCapability:(SDLWindowCapability *)windowCapability defaultSubmenuLayout:(SDLMenuLayout)defaultSubmenuLayout; + +/// Generate SDLAddCommand and SDLAddSubMenu RPCs for the given submenu cells +/// @param cells The cells to generate AddCommand / AddSubMenu RPCs for +/// @param fileManager The file manager to use to check availability of artworks +/// @param windowCapability The window capability with which to check available text fields / image fields +/// @param defaultSubmenuLayout The default submenu layout to use for displaying submenus ++ (NSArray *)subMenuCommandsForCells:(NSArray *)cells fileManager:(SDLFileManager *)fileManager windowCapability:(SDLWindowCapability *)windowCapability defaultSubmenuLayout:(SDLMenuLayout)defaultSubmenuLayout; + +#pragma mark - Updating Menu Cells + +/// Find the menu cell given a command id and remove it from the list (or a cell in the list's subcell list, etc.) +/// @param menuCellList The list to mutate and remove the item from +/// @param commandId The id of the cell to find and remove +/// @return YES if the cell was found and removed successfully, NO if it was not ++ (BOOL)removeCellFromList:(NSMutableArray *)menuCellList withCellId:(UInt32)commandId; + +/// Finds a menu cell from newMenuList with the given commandId and inserts it into the main menu list (or a subcell list) at the given position +/// @param commandId The command id for the cell to be found +/// @param position The position to insert the cell into the appropriate list for it to be in +/// @param newMenuList The complete requested new menu list. We will find the cell to insert from this list. +/// @param mainMenuList The mutable main menu list. The place to insert the cell will be in this list or one of its cell's subcell list (or one of it's cell's subcell's subcell's list, etc.) +/// @return YES if the cell was added successfully, NO if the cell was not ++ (BOOL)addCellWithCellId:(UInt32)commandId position:(UInt16)position fromNewMenuList:(NSArray *)newMenuList toMainMenuList:(NSMutableArray *)mainMenuList; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLink/private/SDLMenuReplaceUtilities.m b/SmartDeviceLink/private/SDLMenuReplaceUtilities.m new file mode 100644 index 000000000..4977a4794 --- /dev/null +++ b/SmartDeviceLink/private/SDLMenuReplaceUtilities.m @@ -0,0 +1,344 @@ +// +// SDLMenuReplaceUtilities.m +// SmartDeviceLink +// +// Created by Joel Fischer on 1/22/21. +// Copyright © 2021 smartdevicelink. All rights reserved. +// + +#import "SDLMenuReplaceUtilities.h" + +#import "SDLAddCommand.h" +#import "SDLAddSubMenu.h" +#import "SDLArtwork.h" +#import "SDLDeleteCommand.h" +#import "SDLDeleteSubMenu.h" +#import "SDLFileManager.h" +#import "SDLImage.h" +#import "SDLImageFieldName.h" +#import "SDLMenuCell.h" +#import "SDLMenuParams.h" +#import "SDLMenuManagerPrivateConstants.h" +#import "SDLRPCRequest.h" +#import "SDLWindowCapability.h" +#import "SDLWindowCapability+ScreenManagerExtensions.h" + +@interface SDLMenuCell () + +@property (assign, nonatomic) UInt32 parentCellId; +@property (assign, nonatomic) UInt32 cellId; +@property (copy, nonatomic, readwrite, nullable) NSArray *subCells; +@property (copy, nonatomic, readwrite, nullable) SDLMenuCellSelectionHandler handler; + +@end + +@interface SDLMenuReplaceUtilities () + +@property (class, assign, nonatomic) UInt32 nextMenuId; + +@end + +@implementation SDLMenuReplaceUtilities +static UInt32 _menuId = 0; + +#pragma mark Ids + ++ (void)setNextMenuId:(UInt32)nextMenuId { + _menuId = nextMenuId; +} + ++ (UInt32)nextMenuId { + return ++_menuId; +} + +/// Assign cell ids on an array of menu cells given a parent id (or no parent id) +/// @param menuCells The array of menu cells to update +/// @param parentId The parent id to assign if needed ++ (void)addIdsToMenuCells:(NSArray *)menuCells parentId:(UInt32)parentId { + for (SDLMenuCell *cell in menuCells) { + cell.cellId = self.class.nextMenuId; + if (parentId != ParentIdNotFound) { + cell.parentCellId = parentId; + } + if (cell.subCells.count > 0) { + [self addIdsToMenuCells:cell.subCells parentId:cell.cellId]; + } + } +} + ++ (void)transferCellIDsFromCells:(NSArray *)fromCells toCells:(NSArray *)toCells { + if (fromCells.count == 0 || fromCells.count != toCells.count) { return; } + for (NSUInteger i = 0; i < toCells.count; i++) { + toCells[i].cellId = fromCells[i].cellId; + } + + // Update parent ids + for (SDLMenuCell *cell in toCells) { + if (cell.subCells == nil) { continue; } + + for (SDLMenuCell *subCell in cell.subCells) { + subCell.parentCellId = cell.cellId; + } + } +} + ++ (void)transferCellHandlersFromCells:(NSArray *)fromCells toCells:(NSArray *)toCells { + if (fromCells.count == 0 || fromCells.count != toCells.count) { return; } + for (NSUInteger i = 0; i < toCells.count; i++) { + toCells[i].handler = fromCells[i].handler; + } +} + +#pragma mark Artworks + ++ (NSArray *)findAllArtworksToBeUploadedFromCells:(NSArray *)cells fileManager:(SDLFileManager *)fileManager windowCapability:(SDLWindowCapability *)windowCapability { + if (![windowCapability hasImageFieldOfName:SDLImageFieldNameCommandIcon]) { return @[]; } + + NSMutableSet *mutableArtworks = [NSMutableSet set]; + for (SDLMenuCell *cell in cells) { + if ((cell.icon != nil) && [fileManager fileNeedsUpload:cell.icon]) { + [mutableArtworks addObject:cell.icon]; + } + + if ((cell.secondaryArtwork != nil) && [fileManager fileNeedsUpload:cell.secondaryArtwork]) { + [mutableArtworks addObject:cell.secondaryArtwork]; + } + + if (cell.subCells.count > 0) { + [mutableArtworks addObjectsFromArray:[self findAllArtworksToBeUploadedFromCells:cell.subCells fileManager:fileManager windowCapability:windowCapability]]; + } + } + + return [mutableArtworks allObjects]; +} + +/// If there is an icon and the icon has been uploaded, or if the icon is a static icon, it should include the image ++ (BOOL)sdl_shouldCellIncludePrimaryImageFromCell:(SDLMenuCell *)cell fileManager:(SDLFileManager *)fileManager windowCapability:(SDLWindowCapability *)windowCapability { + BOOL supportsImage = (cell.subCells != nil) ? [windowCapability hasImageFieldOfName:SDLImageFieldNameSubMenuIcon] : [windowCapability hasImageFieldOfName:SDLImageFieldNameCommandIcon]; + return (cell.icon != nil) && supportsImage && ([fileManager hasUploadedFile:cell.icon] || cell.icon.isStaticIcon); +} + +/// If there is an icon and the icon has been uploaded, or if the icon is a static icon, it should include the image ++ (BOOL)sdl_shouldCellIncludeSecondaryImageFromCell:(SDLMenuCell *)cell fileManager:(SDLFileManager *)fileManager windowCapability:(SDLWindowCapability *)windowCapability { + BOOL supportsImage = (cell.subCells != nil) ? [windowCapability hasImageFieldOfName:SDLImageFieldNameMenuSubMenuSecondaryImage] : [windowCapability hasImageFieldOfName:SDLImageFieldNameMenuCommandSecondaryImage]; + return (cell.secondaryArtwork != nil) && supportsImage && ([fileManager hasUploadedFile:cell.secondaryArtwork] || cell.secondaryArtwork.isStaticIcon); +} + +#pragma mark - RPC Commands +#pragma mark Retrieving Values + ++ (UInt32)commandIdForRPCRequest:(SDLRPCRequest *)request { + UInt32 commandId = 0; + if ([request isMemberOfClass:[SDLAddCommand class]]) { + commandId = ((SDLAddCommand *)request).cmdID.unsignedIntValue; + } else if ([request isMemberOfClass:[SDLAddSubMenu class]]) { + commandId = ((SDLAddSubMenu *)request).menuID.unsignedIntValue; + } else if ([request isMemberOfClass:[SDLDeleteCommand class]]) { + commandId = ((SDLDeleteCommand *)request).cmdID.unsignedIntValue; + } else if ([request isMemberOfClass:[SDLDeleteSubMenu class]]) { + commandId = ((SDLDeleteSubMenu *)request).menuID.unsignedIntValue; + } + + return commandId; +} + ++ (UInt16)positionForRPCRequest:(SDLRPCRequest *)request { + UInt16 position = 0; + if ([request isMemberOfClass:[SDLAddCommand class]]) { + position = ((SDLAddCommand *)request).menuParams.position.unsignedShortValue; + } else if ([request isMemberOfClass:[SDLAddSubMenu class]]) { + position = ((SDLAddSubMenu *)request).position.unsignedShortValue; + } + + return position; +} + +#pragma mark Generating RPCs + ++ (NSArray *)deleteCommandsForCells:(NSArray *)cells { + NSMutableArray *mutableDeletes = [NSMutableArray array]; + for (SDLMenuCell *cell in cells) { + if (cell.subCells != nil) { + SDLDeleteSubMenu *delete = [[SDLDeleteSubMenu alloc] initWithId:cell.cellId]; + [mutableDeletes addObject:delete]; + } else { + SDLDeleteCommand *delete = [[SDLDeleteCommand alloc] initWithId:cell.cellId]; + [mutableDeletes addObject:delete]; + } + } + + return [mutableDeletes copy]; +} + ++ (NSArray *)mainMenuCommandsForCells:(NSArray *)cells fileManager:(SDLFileManager *)fileManager usingPositionsFromFullMenu:(NSArray *)menu windowCapability:(SDLWindowCapability *)windowCapability defaultSubmenuLayout:(SDLMenuLayout)defaultSubmenuLayout { + NSMutableArray *mutableCommands = [NSMutableArray array]; + for (NSUInteger menuInteger = 0; menuInteger < menu.count; menuInteger++) { + for (NSUInteger updateCellsIndex = 0; updateCellsIndex < cells.count; updateCellsIndex++) { + if ([menu[menuInteger] isEqual:cells[updateCellsIndex]]) { + if (cells[updateCellsIndex].subCells != nil) { + [mutableCommands addObject:[self sdl_subMenuCommandForMenuCell:cells[updateCellsIndex] fileManager:fileManager position:(UInt16)menuInteger windowCapability:windowCapability defaultSubmenuLayout:defaultSubmenuLayout]]; + } else { + [mutableCommands addObject:[self sdl_commandForMenuCell:cells[updateCellsIndex] fileManager:fileManager windowCapability:windowCapability position:(UInt16)menuInteger]]; + } + break; + } + } + } + + return [mutableCommands copy]; +} + ++ (NSArray *)subMenuCommandsForCells:(NSArray *)cells fileManager:(SDLFileManager *)fileManager windowCapability:(SDLWindowCapability *)windowCapability defaultSubmenuLayout:(SDLMenuLayout)defaultSubmenuLayout { + NSMutableArray *mutableCommands = [NSMutableArray array]; + for (SDLMenuCell *cell in cells) { + if (cell.subCells != nil) { + [mutableCommands addObjectsFromArray:[self sdl_allCommandsForCells:cell.subCells fileManager:fileManager windowCapability:windowCapability defaultSubmenuLayout:defaultSubmenuLayout]]; + } + } + + return [mutableCommands copy]; +} + +#pragma mark Private Helpers + ++ (NSArray *)sdl_allCommandsForCells:(NSArray *)cells fileManager:(SDLFileManager *)fileManager windowCapability:(SDLWindowCapability *)windowCapability defaultSubmenuLayout:(SDLMenuLayout)defaultSubmenuLayout { + NSMutableArray *mutableCommands = [NSMutableArray array]; + + for (NSUInteger cellIndex = 0; cellIndex < cells.count; cellIndex++) { + if (cells[cellIndex].subCells != nil) { + [mutableCommands addObject:[self sdl_subMenuCommandForMenuCell:cells[cellIndex] fileManager:fileManager position:(UInt16)cellIndex windowCapability:windowCapability defaultSubmenuLayout:defaultSubmenuLayout]]; + [mutableCommands addObjectsFromArray:[self sdl_allCommandsForCells:cells[cellIndex].subCells fileManager:fileManager windowCapability:windowCapability defaultSubmenuLayout:defaultSubmenuLayout]]; + } else { + [mutableCommands addObject:[self sdl_commandForMenuCell:cells[cellIndex] fileManager:fileManager windowCapability:windowCapability position:(UInt16)cellIndex]]; + } + } + + return [mutableCommands copy]; +} + ++ (SDLAddCommand *)sdl_commandForMenuCell:(SDLMenuCell *)cell fileManager:(SDLFileManager *)fileManager windowCapability:(SDLWindowCapability *)windowCapability position:(UInt16)position { + SDLAddCommand *command = [[SDLAddCommand alloc] init]; + + SDLMenuParams *params = [[SDLMenuParams alloc] init]; + params.menuName = cell.uniqueTitle; + params.secondaryText = ([windowCapability hasTextFieldOfName:SDLTextFieldNameMenuCommandSecondaryText] && cell.secondaryText.length > 0) ? cell.secondaryText : nil; + params.tertiaryText = ([windowCapability hasTextFieldOfName:SDLTextFieldNameMenuCommandTertiaryText] && cell.tertiaryText.length > 0) ? cell.tertiaryText : nil; + params.parentID = (cell.parentCellId != ParentIdNotFound) ? @(cell.parentCellId) : nil; + params.position = @(position); + + command.menuParams = params; + command.vrCommands = (cell.voiceCommands.count == 0) ? nil : cell.voiceCommands; + command.cmdIcon = [self sdl_shouldCellIncludePrimaryImageFromCell:cell fileManager:fileManager windowCapability:windowCapability] ? cell.icon.imageRPC : nil; + command.secondaryImage = [self sdl_shouldCellIncludeSecondaryImageFromCell:cell fileManager:fileManager windowCapability:windowCapability] ? cell.secondaryArtwork.imageRPC : nil; + command.cmdID = @(cell.cellId); + + return command; +} + ++ (SDLAddSubMenu *)sdl_subMenuCommandForMenuCell:(SDLMenuCell *)cell fileManager:(SDLFileManager *)fileManager position:(UInt16)position windowCapability:(SDLWindowCapability *)windowCapability defaultSubmenuLayout:(SDLMenuLayout)defaultSubmenuLayout { + NSString *secondaryText = ([windowCapability hasTextFieldOfName:SDLTextFieldNameMenuCommandSecondaryText] && cell.secondaryText.length > 0) ? cell.secondaryText : nil; + NSString *tertiaryText = ([windowCapability hasTextFieldOfName:SDLTextFieldNameMenuCommandTertiaryText] && cell.tertiaryText.length > 0) ? cell.tertiaryText : nil; + SDLImage *icon = [self sdl_shouldCellIncludePrimaryImageFromCell:cell fileManager:fileManager windowCapability:windowCapability] ? cell.icon.imageRPC : nil; + SDLImage *secondaryIcon = [self sdl_shouldCellIncludeSecondaryImageFromCell:cell fileManager:fileManager windowCapability:windowCapability] ? cell.secondaryArtwork.imageRPC : nil; + + SDLMenuLayout submenuLayout = nil; + if (cell.submenuLayout && [windowCapability.menuLayoutsAvailable containsObject:cell.submenuLayout]) { + submenuLayout = cell.submenuLayout; + } else { + submenuLayout = defaultSubmenuLayout; + } + + return [[SDLAddSubMenu alloc] initWithMenuID:cell.cellId menuName:cell.uniqueTitle position:@(position) menuIcon:icon menuLayout:submenuLayout parentID:nil secondaryText:secondaryText tertiaryText:tertiaryText secondaryImage:secondaryIcon]; +} + +#pragma mark - Updating Menu Cells In Lists + ++ (BOOL)removeCellFromList:(NSMutableArray *)menuCellList withCellId:(UInt32)commandId { + for (SDLMenuCell *menuCell in menuCellList) { + if (menuCell.cellId == commandId) { + // If the cell id matches the command id, remove it from the list and return + [menuCellList removeObject:menuCell]; + return YES; + } else if (menuCell.subCells.count > 0) { + // If the menu cell has subcells, we need to recurse and check the subcells + NSMutableArray *newList = [menuCell.subCells mutableCopy]; + BOOL foundAndRemovedItem = [self removeCellFromList:newList withCellId:commandId]; + if (foundAndRemovedItem) { + menuCell.subCells = [newList copy]; + return YES; + } + } + } + + return NO; +} + ++ (BOOL)addCellWithCellId:(UInt32)cellId position:(UInt16)position fromNewMenuList:(NSArray *)newMenuList toMainMenuList:(NSMutableArray *)mainMenuList { + SDLMenuCell *addedCell = nil; + for (SDLMenuCell *cell in newMenuList) { + if (cell.cellId == cellId) { + addedCell = cell; + break; + } else if (cell.subCells.count > 0) { + BOOL success = [self addCellWithCellId:cellId position:position fromNewMenuList:cell.subCells toMainMenuList:mainMenuList]; + if (success) { return YES; } + } + } + + if (addedCell != nil) { + return [self sdl_addMenuCell:addedCell toList:mainMenuList atPosition:position]; + } + + return NO; +} + ++ (BOOL)sdl_addMenuCell:(SDLMenuCell *)cell toList:(NSMutableArray *)menuCellList atPosition:(UInt16)position { + if (cell.parentCellId == ParentIdNotFound) { + // The cell does not have a parent id, just insert it into the main menu + [self sdl_insertMenuCell:cell intoList:menuCellList atPosition:position]; + return YES; + } + + // If the cell has a parent id, we need to find the cell with a matching cell id and insert it into its submenu + for (SDLMenuCell *menuCell in menuCellList) { + if (menuCell.cellId == cell.parentCellId) { + // If we found the correct submenu, insert it into that submenu + NSMutableArray *newList = nil; + if (menuCell.subCells != nil) { + newList = [menuCell.subCells mutableCopy]; + } else { + newList = [NSMutableArray array]; + } + + [self sdl_insertMenuCell:cell intoList:newList atPosition:position]; + menuCell.subCells = [newList copy]; + return YES; + } else if (menuCell.subCells.count > 0) { + // Check the subcells of this cell to see if any of those have cell ids that match the parent cell id + NSMutableArray *newList = [menuCell.subCells mutableCopy]; + BOOL foundAndAddedItem = [self sdl_addMenuCell:cell toList:newList atPosition:position]; + if (foundAndAddedItem) { + menuCell.subCells = [newList copy]; + return YES; + } + } + } + + return NO; +} + ++ (void)sdl_insertMenuCell:(SDLMenuCell *)cell intoList:(NSMutableArray *)cellList atPosition:(UInt16)position { + SDLMenuCell *cellToInsert = cell; + if (cellToInsert.subCells != nil) { + cellToInsert = [cell copy]; + cellToInsert.subCells = @[]; + } + + if (position > cellList.count) { + [cellList addObject:cellToInsert]; + } else { + [cellList insertObject:cellToInsert atIndex:position]; + } +} + +@end diff --git a/SmartDeviceLink/private/SDLMenuShowOperation.h b/SmartDeviceLink/private/SDLMenuShowOperation.h new file mode 100644 index 000000000..b79e3d163 --- /dev/null +++ b/SmartDeviceLink/private/SDLMenuShowOperation.h @@ -0,0 +1,25 @@ +// +// SDLMenuShowOperation.h +// SmartDeviceLink +// +// Created by Joel Fischer on 1/21/21. +// Copyright © 2021 smartdevicelink. All rights reserved. +// + +#import + +#import "SDLAsynchronousOperation.h" +#import "SDLConnectionManagerType.h" + +@class SDLMenuCell; + +NS_ASSUME_NONNULL_BEGIN + +@interface SDLMenuShowOperation : SDLAsynchronousOperation +typedef void(^SDLMenuShowCompletionBlock)(NSError *_Nullable error); + +- (instancetype)initWithConnectionManager:(id)connectionManager toMenuCell:(nullable SDLMenuCell *)menuCell completionHandler:(SDLMenuShowCompletionBlock)completionHandler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLink/private/SDLMenuShowOperation.m b/SmartDeviceLink/private/SDLMenuShowOperation.m new file mode 100644 index 000000000..92fa5a971 --- /dev/null +++ b/SmartDeviceLink/private/SDLMenuShowOperation.m @@ -0,0 +1,105 @@ +// +// SDLMenuShowOperation.m +// SmartDeviceLink +// +// Created by Joel Fischer on 1/21/21. +// Copyright © 2021 smartdevicelink. All rights reserved. +// + +#import "SDLMenuShowOperation.h" + +#import "SDLError.h" +#import "SDLGlobals.h" +#import "SDLLogMacros.h" +#import "SDLMenuCell.h" +#import "SDLRPCResponse.h" +#import "SDLResult.h" +#import "SDLShowAppMenu.h" +#import "SDLVersion.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface SDLMenuCell() + +@property (assign, nonatomic) UInt32 parentCellId; +@property (assign, nonatomic) UInt32 cellId; + +@end + +@interface SDLMenuShowOperation () + +@property (weak, nonatomic) id connectionManager; +@property (strong, nonatomic, nullable) SDLMenuCell *submenuCell; +@property (copy, nonatomic) SDLMenuShowCompletionBlock showCompletionHandler; + +@property (copy, nonatomic, nullable) NSError *internalError; + +@end + +@implementation SDLMenuShowOperation + +- (instancetype)initWithConnectionManager:(id)connectionManager toMenuCell:(nullable SDLMenuCell *)menuCell completionHandler:(nonnull SDLMenuShowCompletionBlock)completionHandler { + self = [super init]; + if (!self) { return nil; } + + _connectionManager = connectionManager; + _submenuCell = menuCell; + _showCompletionHandler = completionHandler; + + return self; +} + +- (void)start { + [super start]; + if (self.isCancelled) { return; } + + SDLShowAppMenu *openMenu = nil; + if (self.submenuCell != nil) { + openMenu = [[SDLShowAppMenu alloc] initWithMenuID:self.submenuCell.cellId]; + } else { + openMenu = [[SDLShowAppMenu alloc] init]; + } + + __weak typeof(self) weakself = self; + [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 with error: %@", error); + } else if (![response.resultCode isEqualToEnum:SDLResultSuccess]) { + SDLLogE(@"Error opening application menu with error: %@", error); + self.internalError = [NSError sdl_menuManager_openMenuOperationFailed:weakself.submenuCell]; + } else { + SDLLogD(@"Successfully opened application menu"); + } + + [self finishOperation]; + }]; +} + +#pragma mark - Operation Overrides + +- (void)finishOperation { + SDLLogV(@"Finishing menu manager configuration update operation"); + if (self.isCancelled) { + self.internalError = [NSError sdl_menuManager_openMenuOperationCancelled]; + } + + self.showCompletionHandler(self.internalError); + + [super finishOperation]; +} + +- (nullable NSString *)name { + return @"com.sdl.menuManager.openMenu"; +} + +- (NSOperationQueuePriority)queuePriority { + return NSOperationQueuePriorityNormal; +} + +- (nullable NSError *)error { + return self.internalError; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLink/private/SDLVoiceCommandManager.m b/SmartDeviceLink/private/SDLVoiceCommandManager.m index 3d7f51dd5..b4990b263 100644 --- a/SmartDeviceLink/private/SDLVoiceCommandManager.m +++ b/SmartDeviceLink/private/SDLVoiceCommandManager.m @@ -12,6 +12,7 @@ #import "SDLConnectionManagerType.h" #import "SDLDeleteCommand.h" #import "SDLError.h" +#import "SDLGlobals.h" #import "SDLHMILevel.h" #import "SDLLogMacros.h" #import "SDLNotificationConstants.h" @@ -87,6 +88,7 @@ - (NSOperationQueue *)sdl_newTransactionQueue { queue.name = @"SDLVoiceCommandManager Transaction Queue"; queue.maxConcurrentOperationCount = 1; queue.qualityOfService = NSQualityOfServiceUserInitiated; + queue.underlyingQueue = [SDLGlobals sharedGlobals].sdlConcurrentQueue; queue.suspended = YES; return queue; @@ -210,9 +212,7 @@ - (void)sdl_commandNotification:(SDLRPCNotificationNotification *)notification { - (void)sdl_hmiStatusNotification:(SDLRPCNotificationNotification *)notification { SDLOnHMIStatus *hmiStatus = (SDLOnHMIStatus *)notification.notification; - if (hmiStatus.windowID != nil && hmiStatus.windowID.integerValue != SDLPredefinedWindowsDefaultWindow) { - return; - } + if ((hmiStatus.windowID != nil) && (hmiStatus.windowID.integerValue != SDLPredefinedWindowsDefaultWindow)) { return; } self.currentLevel = hmiStatus.hmiLevel; [self sdl_updateTransactionQueueSuspended]; diff --git a/SmartDeviceLink/private/SDLVoiceCommandUpdateOperation.m b/SmartDeviceLink/private/SDLVoiceCommandUpdateOperation.m index 8cf04c400..548c3fbcb 100644 --- a/SmartDeviceLink/private/SDLVoiceCommandUpdateOperation.m +++ b/SmartDeviceLink/private/SDLVoiceCommandUpdateOperation.m @@ -68,10 +68,7 @@ - (void)start { __weak typeof(self) weakSelf = self; [self sdl_sendDeleteCurrentVoiceCommands:^{ // If the operation has been canceled, then don't send the new commands and finish the operation - if (self.isCancelled) { - [weakSelf finishOperation]; - return; - } + if (self.isCancelled) { return [weakSelf finishOperation]; } // Send the new commands [weakSelf sdl_sendCurrentVoiceCommands:^{ diff --git a/SmartDeviceLink/public/SDLErrorConstants.h b/SmartDeviceLink/public/SDLErrorConstants.h index 28a43a263..a148e9f57 100644 --- a/SmartDeviceLink/public/SDLErrorConstants.h +++ b/SmartDeviceLink/public/SDLErrorConstants.h @@ -193,7 +193,11 @@ typedef NS_ENUM(NSInteger, SDLSubscribeButtonManagerError) { typedef NS_ENUM(NSInteger, SDLMenuManagerError) { /// Sending menu-related RPCs returned an error from the remote system SDLMenuManagerErrorRPCsFailed = -1, - SDLMenuManagerErrorPendingUpdateSuperseded = -2 + SDLMenuManagerErrorPendingUpdateSuperseded = -2, + SDLMenuManagerErrorOperationCancelled = -3, + SDLMenuManagerErrorConfigurationUpdateLayoutNotSupported = -4, + SDLMenuManagerErrorConfigurationUpdateFailed = -5, + SDLMenuManagerErrorOpenMenuFailed = -6 }; /// Errors associated with Choice Set Manager class diff --git a/SmartDeviceLink/public/SDLMenuCell.h b/SmartDeviceLink/public/SDLMenuCell.h index fc658725e..18c49c40c 100644 --- a/SmartDeviceLink/public/SDLMenuCell.h +++ b/SmartDeviceLink/public/SDLMenuCell.h @@ -125,6 +125,11 @@ typedef void(^SDLMenuCellSelectionHandler)(SDLTriggerSource triggerSource); */ - (instancetype)initWithTitle:(NSString *)title secondaryText:(nullable NSString *)secondaryText tertiaryText:(nullable NSString *)tertiaryText icon:(nullable SDLArtwork *)icon secondaryArtwork:(nullable SDLArtwork *)secondaryArtwork submenuLayout:(nullable SDLMenuLayout)layout subCells:(NSArray *)subCells; +/// Check cell equality including the internally used `uniqueTitle` property. To compare without `uniqueTitle`, use the `isEqual:` method. +/// @param cell The other cell to compare with +/// @return True if the cells are equal, including the `uniqueTitle` property, False otherwise +- (BOOL)isEqualToCellWithUniqueTitle:(SDLMenuCell *)cell; + @end NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLink/public/SDLMenuCell.m b/SmartDeviceLink/public/SDLMenuCell.m index db473b643..bbc02f2b6 100644 --- a/SmartDeviceLink/public/SDLMenuCell.m +++ b/SmartDeviceLink/public/SDLMenuCell.m @@ -8,8 +8,10 @@ #import "SDLMenuCell.h" -#import "SDLArtwork.h" #import "NSArray+Extensions.h" +#import "SDLArtwork.h" +#import "SDLMacros.h" +#import "SDLWindowCapability+ScreenManagerExtensions.h" NS_ASSUME_NONNULL_BEGIN @@ -90,24 +92,14 @@ - (NSString *)debugDescription { #pragma mark - Object Equality -- (id)copyWithZone:(nullable NSZone *)zone { - SDLMenuCell *newCell = [[SDLMenuCell allocWithZone:zone] initWithTitle:_title secondaryText:_secondaryText tertiaryText:_tertiaryText icon:_icon secondaryArtwork:_secondaryArtwork voiceCommands:_voiceCommands handler:_handler]; - - if (_subCells.count > 0) { - newCell.subCells = [[NSArray alloc] initWithArray:_subCells copyItems:YES]; - } - - return newCell; -} - - (NSUInteger)hash { return NSUIntRotateCell(self.title.hash, NSUIntBitCell / 2) ^ NSUIntRotateCell(self.icon.name.hash, NSUIntBitCell / 3) ^ 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) + ^ NSUIntRotateCell((self.subCells != nil), NSUIntBitCell / 5) + ^ NSUIntRotateCell(self.secondaryText.hash, NSUIntBitCell / 6) + ^ NSUIntRotateCell(self.tertiaryText.hash, NSUIntBitCell / 7) + ^ NSUIntRotateCell(self.secondaryArtwork.name.hash, NSUIntBitCell / 8) ^ NSUIntRotateCell(self.submenuLayout.hash, NSUIntBitCell / 9); } @@ -115,13 +107,36 @@ - (BOOL)isEqual:(id)object { if (self == object) { return YES; } if (![object isMemberOfClass:[self class]]) { return NO; } - return [self isEqualToChoice:(SDLMenuCell *)object]; + return [self isEqualToCell:(SDLMenuCell *)object]; +} + +- (BOOL)isEqualToCell:(SDLMenuCell *)cell { + return (self.hash == cell.hash); } -- (BOOL)isEqualToChoice:(SDLMenuCell *)choice { - if (choice == nil) { return NO; } +#pragma mark With Unique Title - return (self.hash == choice.hash); +- (BOOL)isEqualToCellWithUniqueTitle:(SDLMenuCell *)cell { + return ([self sdl_hashWithUniqueTitle] == [cell sdl_hashWithUniqueTitle]); +} + +- (NSUInteger)sdl_hashWithUniqueTitle { + return self.hash ^ NSUIntRotateCell(self.uniqueTitle.hash, NSUIntBitCell / 10); +} + +#pragma mark - Copying + +- (id)copyWithZone:(nullable NSZone *)zone { + SDLMenuCell *newCell = [[SDLMenuCell allocWithZone:zone] initWithTitle:_title secondaryText:_secondaryText tertiaryText:_tertiaryText icon:_icon secondaryArtwork:_secondaryArtwork voiceCommands:_voiceCommands handler:_handler]; + newCell->_cellId = _cellId; + newCell->_parentCellId = _parentCellId; + newCell->_uniqueTitle = _uniqueTitle; + + if (_subCells.count > 0) { + newCell.subCells = [[NSArray alloc] initWithArray:_subCells copyItems:YES]; + } + + return newCell; } @end diff --git a/SmartDeviceLink/public/SDLMenuConfiguration.m b/SmartDeviceLink/public/SDLMenuConfiguration.m index 2049c83bf..b957d72ca 100644 --- a/SmartDeviceLink/public/SDLMenuConfiguration.m +++ b/SmartDeviceLink/public/SDLMenuConfiguration.m @@ -8,6 +8,8 @@ #import "SDLMenuConfiguration.h" +#import "SDLMacros.h" + @implementation SDLMenuConfiguration - (instancetype)init { @@ -24,6 +26,24 @@ - (instancetype)initWithMainMenuLayout:(SDLMenuLayout)mainMenuLayout defaultSubm return self; } +- (NSUInteger)hash { + return NSUIntRotateCell(self.mainMenuLayout.hash, NSUIntBitCell / 2) + ^ NSUIntRotateCell(self.defaultSubmenuLayout.hash, NSUIntBitCell / 3); +} + +- (BOOL)isEqual:(id)object { + if (self == object) { return YES; } + if (![object isMemberOfClass:[self class]]) { return NO; } + + return [self isEqualToConfiguration:(SDLMenuConfiguration *)object]; +} + +- (BOOL)isEqualToConfiguration:(SDLMenuConfiguration *)configuration { + if (configuration == nil) { return NO; } + + return (self.hash == configuration.hash); +} + - (NSString *)description { return [NSString stringWithFormat:@"Menu configuration, main menu layout: %@, submenu default layout: %@", _mainMenuLayout, _defaultSubmenuLayout]; } diff --git a/SmartDeviceLink/public/SDLMenuManagerConstants.h b/SmartDeviceLink/public/SDLMenuManagerConstants.h index 5e23971e9..5ceab2d4f 100644 --- a/SmartDeviceLink/public/SDLMenuManagerConstants.h +++ b/SmartDeviceLink/public/SDLMenuManagerConstants.h @@ -22,18 +22,3 @@ typedef NS_ENUM(NSUInteger, SDLDynamicMenuUpdatesMode) { /// This mode checks whether the phone is connected to a SYNC Gen 3 head unit, which has known menu ordering issues. If it is, it will always delete and re-add every menu item, if not, it will dynamically update the menus. SDLDynamicMenuUpdatesModeOnWithCompatibility }; - - -/// Menu cell state -/// -/// Cell state that tells the menu manager what it should do with a given SDLMenuCell -typedef NS_ENUM(NSUInteger, MenuCellState) { - /// Marks the cell to be deleted - MenuCellStateDelete = 0, - - /// Marks the cell to be added - MenuCellStateAdd, - - /// Marks the cell to be kept - MenuCellStateKeep -}; diff --git a/SmartDeviceLink/public/SDLScreenManager.m b/SmartDeviceLink/public/SDLScreenManager.m index ea60f9584..d413cdee6 100644 --- a/SmartDeviceLink/public/SDLScreenManager.m +++ b/SmartDeviceLink/public/SDLScreenManager.m @@ -332,11 +332,11 @@ - (void)dismissKeyboardWithCancelID:(NSNumber *)cancelID{ #pragma mark - Menu - (BOOL)openMenu { - return [self.menuManager openMenu]; + return [self.menuManager openMenu:nil]; } - (BOOL)openSubmenu:(SDLMenuCell *)cell { - return [self.menuManager openSubmenu:cell]; + return [self.menuManager openMenu:cell]; } #pragma mark - Alert diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLMenuCellSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuCellSpec.m index f6d982a57..4958e7674 100644 --- a/SmartDeviceLinkTests/DevAPISpecs/SDLMenuCellSpec.m +++ b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuCellSpec.m @@ -108,6 +108,13 @@ expect([testCell isEqual:testCell2]).to(beFalse()); }); + it(@"should compare cells and return false if one cell has subcells empty and another has subcells nil", ^{ + testCell = [[SDLMenuCell alloc] initWithTitle:someTitle secondaryText:someSecondaryTitle tertiaryText:someTertiaryTitle icon:nil secondaryArtwork:someSecondaryArtwork submenuLayout:testLayout subCells:nil]; + testCell2 = [[SDLMenuCell alloc] initWithTitle:someTitle secondaryText:someSecondaryTitle tertiaryText:someTertiaryTitle icon:nil secondaryArtwork:someSecondaryArtwork submenuLayout:testLayout subCells:@[]]; + + expect([testCell isEqual:testCell2]).to(beFalse()); + }); + it(@"should compare cells and return true if cells equal", ^{ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLMenuConfigurationUpdateOperationSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuConfigurationUpdateOperationSpec.m new file mode 100644 index 000000000..3678d4ecc --- /dev/null +++ b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuConfigurationUpdateOperationSpec.m @@ -0,0 +1,174 @@ +// +// SDLMenuConfigurationUpdateOperationSpec.m +// SmartDeviceLinkTests +// +// Created by Joel Fischer on 2/16/21. +// Copyright © 2021 smartdevicelink. All rights reserved. +// + +#import +#import +#import + +#import +#import "SDLMenuConfigurationUpdateOperation.h" +#import "TestConnectionManager.h" + +QuickSpecBegin(SDLMenuConfigurationUpdateOperationSpec) + +describe(@"a menu configuration update operation", ^{ + __block SDLMenuConfigurationUpdateOperation *testOp = nil; + + __block TestConnectionManager *testConnectionManager = nil; + __block SDLFileManager *testFileManager = nil; + __block SDLWindowCapability *testWindowCapability = nil; + SDLMenuConfiguration *testMenuConfiguration = [[SDLMenuConfiguration alloc] initWithMainMenuLayout:SDLMenuLayoutList defaultSubmenuLayout:SDLMenuLayoutTiles]; + + __block SDLMenuConfigurationUpdatedBlock testUpdatedBlock = nil; + __block SDLMenuConfiguration *resultMenuConfiguration = nil; + __block NSError *resultError = nil; + + beforeEach(^{ + testConnectionManager = [[TestConnectionManager alloc] init]; + testFileManager = OCMClassMock([SDLFileManager class]); + testWindowCapability = [[SDLWindowCapability alloc] initWithWindowID:@0 textFields:nil imageFields:nil imageTypeSupported:nil templatesAvailable:nil numCustomPresetsAvailable:nil buttonCapabilities:nil softButtonCapabilities:nil menuLayoutsAvailable:@[] dynamicUpdateCapabilities:nil keyboardCapabilities:nil]; + + resultMenuConfiguration = nil; + resultError = nil; + testUpdatedBlock = ^(SDLMenuConfiguration *newConfiguration, NSError *_Nullable error) { + resultMenuConfiguration = newConfiguration; + resultError = error; + }; + }); + + // when the layout check fails + describe(@"when the layout check fails", ^{ + // when there are no known menu layouts + context(@"when there are no known menu layouts", ^{ + it(@"should return an error and finish", ^{ + testOp = [[SDLMenuConfigurationUpdateOperation alloc] initWithConnectionManager:testConnectionManager windowCapability:testWindowCapability newMenuConfiguration:testMenuConfiguration configurationUpdatedHandler:testUpdatedBlock]; + [testOp start]; + + expect(testConnectionManager.receivedRequests).to(beEmpty()); + expect(testOp.isFinished).to(beTrue()); + expect(resultMenuConfiguration).to(beNil()); + expect(resultError).toNot(beNil()); + expect(testOp.error).toNot(beNil()); + }); + }); + + // when the set main menu layout is not available + context(@"when the set main menu layout is not available", ^{ + beforeEach(^{ + testWindowCapability.menuLayoutsAvailable = @[SDLMenuLayoutTiles]; + }); + + it(@"should return an error and finish", ^{ + testOp = [[SDLMenuConfigurationUpdateOperation alloc] initWithConnectionManager:testConnectionManager windowCapability:testWindowCapability newMenuConfiguration:testMenuConfiguration configurationUpdatedHandler:testUpdatedBlock]; + [testOp start]; + + expect(testConnectionManager.receivedRequests).to(beEmpty()); + expect(testOp.isFinished).to(beTrue()); + expect(resultMenuConfiguration).to(beNil()); + expect(resultError).toNot(beNil()); + expect(testOp.error).toNot(beNil()); + }); + }); + + // when the set default submenu layout is not available + context(@"when the set default submenu layout is not available", ^{ + beforeEach(^{ + testWindowCapability.menuLayoutsAvailable = @[SDLMenuLayoutList]; + }); + + it(@"should return an error and finish", ^{ + testOp = [[SDLMenuConfigurationUpdateOperation alloc] initWithConnectionManager:testConnectionManager windowCapability:testWindowCapability newMenuConfiguration:testMenuConfiguration configurationUpdatedHandler:testUpdatedBlock]; + [testOp start]; + + expect(testConnectionManager.receivedRequests).to(beEmpty()); + expect(testOp.isFinished).to(beTrue()); + expect(resultMenuConfiguration).to(beNil()); + expect(resultError).toNot(beNil()); + expect(testOp.error).toNot(beNil()); + }); + }); + }); + + // when the set layouts are available + describe(@"when the set layouts are available", ^{ + __block SDLSetGlobalPropertiesResponse *response = [[SDLSetGlobalPropertiesResponse alloc] init]; + + beforeEach(^{ + testWindowCapability.menuLayoutsAvailable = @[SDLMenuLayoutList, SDLMenuLayoutTiles]; + + testOp = [[SDLMenuConfigurationUpdateOperation alloc] initWithConnectionManager:testConnectionManager windowCapability:testWindowCapability newMenuConfiguration:testMenuConfiguration configurationUpdatedHandler:testUpdatedBlock]; + [testOp start]; + }); + + // should send the RPC + it(@"should send the RPC", ^{ + expect(testOp.error).to(beNil()); + expect(testConnectionManager.receivedRequests).toNot(beEmpty()); + expect(testOp.isFinished).to(beFalse()); + expect(resultMenuConfiguration).to(beNil()); + expect(resultError).to(beNil()); + + SDLSetGlobalProperties *receivedSGP = (SDLSetGlobalProperties *)testConnectionManager.receivedRequests[0]; + expect(receivedSGP.menuLayout).to(equal(testMenuConfiguration.mainMenuLayout)); + }); + + // if an error returned + context(@"if an error returned", ^{ + beforeEach(^{ + response.success = @NO; + response.resultCode = SDLResultRejected; + }); + + it(@"should return an error and finish", ^{ + [testConnectionManager respondToLastRequestWithResponse:response]; + + expect(testOp.error).toNot(beNil()); + expect(testConnectionManager.receivedRequests).toNot(beEmpty()); + expect(testOp.isFinished).to(beTrue()); + expect(resultMenuConfiguration).to(beNil()); + }); + }); + + // if it succeeded + context(@"if it succeeded", ^{ + beforeEach(^{ + response.success = @YES; + response.resultCode = SDLResultSuccess; + }); + + it(@"should not return an error and finish", ^{ + [testConnectionManager respondToLastRequestWithResponse:response]; + + expect(testOp.error).to(beNil()); + expect(testConnectionManager.receivedRequests).toNot(beEmpty()); + expect(testOp.isFinished).to(beTrue()); + expect(resultMenuConfiguration).to(equal(testMenuConfiguration)); + expect(resultError).to(beNil()); + }); + }); + }); + + describe(@"cancelling the operation before it starts", ^{ + testOp = [[SDLMenuConfigurationUpdateOperation alloc] initWithConnectionManager:testConnectionManager windowCapability:testWindowCapability newMenuConfiguration:testMenuConfiguration configurationUpdatedHandler:testUpdatedBlock]; + + beforeEach(^{ + [testOp cancel]; + [testOp start]; + }); + + it(@"should finish without any callbacks", ^{ + expect(testOp.error).to(beNil()); + expect(testConnectionManager.receivedRequests).to(beEmpty()); + expect(testOp.isFinished).to(beTrue()); + expect(resultMenuConfiguration).to(beNil()); + expect(resultError).to(beNil()); + }); + }); +}); + +QuickSpecEnd diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLMenuManagerSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuManagerSpec.m index 4b20b08de..c663205e2 100644 --- a/SmartDeviceLinkTests/DevAPISpecs/SDLMenuManagerSpec.m +++ b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuManagerSpec.m @@ -4,9 +4,10 @@ #import +#import "SDLGlobals.h" #import "SDLMenuManager.h" +#import "SDLMenuReplaceOperation.h" #import "TestConnectionManager.h" -#import "SDLGlobals.h" @interface SDLMenuCell() @@ -22,20 +23,13 @@ @interface SDLMenuManager() @property (weak, nonatomic) SDLFileManager *fileManager; @property (weak, nonatomic) SDLSystemCapabilityManager *systemCapabilityManager; -@property (copy, nonatomic, nullable) SDLHMILevel currentHMILevel; -@property (copy, nonatomic, nullable) SDLSystemContext currentSystemContext; +@property (strong, nonatomic) NSOperationQueue *transactionQueue; @property (strong, nonatomic, nullable) SDLWindowCapability *windowCapability; -@property (strong, nonatomic, nullable) NSArray *inProgressUpdate; -@property (assign, nonatomic) BOOL hasQueuedUpdate; -@property (assign, nonatomic) BOOL waitingOnHMIUpdate; -@property (copy, nonatomic) NSArray *waitingUpdateMenuCells; - -@property (assign, nonatomic) UInt32 lastMenuId; -@property (copy, nonatomic) NSArray *oldMenuCells; - -- (BOOL)sdl_shouldRPCsIncludeImages:(NSArray *)cells; -- (void)sdl_displayCapabilityDidUpdate; +@property (copy, nonatomic, nullable) SDLHMILevel currentHMILevel; +@property (copy, nonatomic, nullable) SDLSystemContext currentSystemContext; +@property (copy, nonatomic) NSArray *currentMenuCells; +@property (strong, nonatomic, nullable) SDLMenuConfiguration *currentMenuConfiguration; @end @@ -46,40 +40,15 @@ - (void)sdl_displayCapabilityDidUpdate; __block TestConnectionManager *mockConnectionManager = nil; __block SDLFileManager *mockFileManager = nil; __block SDLSystemCapabilityManager *mockSystemCapabilityManager = nil; - __block SDLArtwork *testArtwork = nil; - __block SDLArtwork *testArtwork2 = nil; - __block SDLArtwork *testArtwork3 = nil; - - __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 SDLImageField *commandIconField = nil; - __block SDLImageField *commandSecondaryArtworkField = nil; - __block SDLImageField *submenuIconField = nil; - __block SDLImageField *subMenuSecondaryArtworkField = nil; - - __block SDLVersion *menuUniquenessActiveVersion = nil; + __block SDLMenuCell *textOnlyCell = nil; + __block SDLMenuCell *submenuCell = 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]; - testArtwork3 = [[SDLArtwork alloc] initWithData:[@"Test data 3" dataUsingEncoding:NSUTF8StringEncoding] name:@"some artwork name" fileExtension:@"png" persistent:NO]; - testArtwork3.overwrite = YES; - 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) {}]; + submenuCell = [[SDLMenuCell alloc] initWithTitle:@"Test 3" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil submenuLayout:nil subCells:@[textOnlyCell]]; testMenuConfiguration = [[SDLMenuConfiguration alloc] initWithMainMenuLayout:SDLMenuLayoutTiles defaultSubmenuLayout:SDLMenuLayoutList]; @@ -88,620 +57,257 @@ - (void)sdl_displayCapabilityDidUpdate; mockSystemCapabilityManager = OCMClassMock([SDLSystemCapabilityManager class]); testManager = [[SDLMenuManager alloc] initWithConnectionManager:mockConnectionManager fileManager:mockFileManager systemCapabilityManager:mockSystemCapabilityManager]; - commandIconField = [[SDLImageField alloc] initWithName:SDLImageFieldNameCommandIcon imageTypeSupported:@[SDLFileTypePNG] imageResolution:nil]; - commandSecondaryArtworkField = [[SDLImageField alloc] initWithName:SDLImageFieldNameMenuCommandSecondaryImage imageTypeSupported:@[SDLFileTypePNG] imageResolution:nil]; - submenuIconField = [[SDLImageField alloc] initWithName:SDLImageFieldNameSubMenuIcon imageTypeSupported:@[SDLFileTypePNG] imageResolution:nil]; - subMenuSecondaryArtworkField = [[SDLImageField alloc] initWithName:SDLImageFieldNameMenuSubMenuSecondaryImage imageTypeSupported:@[SDLFileTypePNG] imageResolution:nil]; - SDLTextField *commandSecondaryTextField = [[SDLTextField alloc] initWithName:SDLTextFieldNameMenuCommandSecondaryText characterSet:SDLCharacterSetAscii width:100 rows:1]; - SDLTextField *commandTertiaryTextField = [[SDLTextField alloc] initWithName:SDLTextFieldNameMenuCommandTertiaryText characterSet:SDLCharacterSetAscii width:100 rows:1]; - SDLTextField *submenuSecondaryTextField = [[SDLTextField alloc] initWithName:SDLTextFieldNameMenuSubMenuSecondaryText characterSet:SDLCharacterSetAscii width:100 rows:1]; - SDLTextField *submenuTertiaryTextField = [[SDLTextField alloc] initWithName:SDLTextFieldNameMenuSubMenuTertiaryText characterSet:SDLCharacterSetAscii width:100 rows:1]; + SDLImageField *commandIconField = [[SDLImageField alloc] init]; + commandIconField.name = SDLImageFieldNameCommandIcon; SDLWindowCapability *windowCapability = [[SDLWindowCapability alloc] init]; windowCapability.windowID = @(SDLPredefinedWindowsDefaultWindow); - windowCapability.imageFields = @[commandIconField, commandSecondaryArtworkField, submenuIconField, subMenuSecondaryArtworkField]; - windowCapability.textFields = @[commandSecondaryTextField, commandTertiaryTextField, submenuSecondaryTextField, submenuTertiaryTextField]; + windowCapability.imageFields = @[commandIconField]; windowCapability.imageTypeSupported = @[SDLImageTypeDynamic, SDLImageTypeStatic]; windowCapability.menuLayoutsAvailable = @[SDLMenuLayoutList, SDLMenuLayoutTiles]; testManager.windowCapability = windowCapability; - menuUniquenessActiveVersion = [[SDLVersion alloc] initWithMajor:7 minor:1 patch:0]; }); - // should instantiate correctly it(@"should instantiate correctly", ^{ expect(testManager.menuCells).to(beEmpty()); + + expect(@(testManager.dynamicMenuUpdatesMode)).to(equal(@(SDLDynamicMenuUpdatesModeOnWithCompatibility))); expect(testManager.connectionManager).to(equal(mockConnectionManager)); expect(testManager.fileManager).to(equal(mockFileManager)); expect(testManager.systemCapabilityManager).to(equal(mockSystemCapabilityManager)); + expect(testManager.transactionQueue).toNot(beNil()); + expect(testManager.windowCapability).toNot(beNil()); expect(testManager.currentHMILevel).to(beNil()); - expect(testManager.inProgressUpdate).to(beNil()); - expect(testManager.hasQueuedUpdate).to(beFalse()); - expect(testManager.waitingOnHMIUpdate).to(beFalse()); - expect(testManager.lastMenuId).to(equal(1)); - expect(testManager.oldMenuCells).to(beEmpty()); - expect(testManager.waitingUpdateMenuCells).to(beNil()); - expect(testManager.menuConfiguration).toNot(beNil()); - }); - - // updating menu cells before HMI is ready - describe(@"updating menu cells before HMI is ready", ^{ - // when in HMI NONE - context(@"when in HMI NONE", ^{ - beforeEach(^{ - testManager.currentHMILevel = SDLHMILevelNone; - testManager.menuCells = @[textOnlyCell]; - }); - - it(@"should not update", ^{ - expect(mockConnectionManager.receivedRequests).to(beEmpty()); - }); - - describe(@"when entering the foreground", ^{ - beforeEach(^{ - SDLOnHMIStatus *onHMIStatus = [[SDLOnHMIStatus alloc] init]; - onHMIStatus.hmiLevel = SDLHMILevelFull; - onHMIStatus.systemContext = SDLSystemContextMain; - - SDLRPCNotificationNotification *testSystemContextNotification = [[SDLRPCNotificationNotification alloc] initWithName:SDLDidChangeHMIStatusNotification object:nil rpcNotification:onHMIStatus]; - [[NSNotificationCenter defaultCenter] postNotification:testSystemContextNotification]; - }); - - it(@"should update", ^{ - expect(mockConnectionManager.receivedRequests).toNot(beEmpty()); - }); - }); - }); - - // when no HMI level has been received - context(@"when no HMI level has been received", ^{ - beforeEach(^{ - testManager.currentHMILevel = nil; - }); - - it(@"should not update the menu configuration", ^{ - testManager.menuConfiguration = testMenuConfiguration; - expect(mockConnectionManager.receivedRequests).to(beEmpty()); - expect(testManager.menuConfiguration).toNot(equal(testMenuConfiguration)); - }); - - it(@"should not update the menu cells", ^{ - testManager.menuCells = @[textOnlyCell]; - expect(mockConnectionManager.receivedRequests).to(beEmpty()); - }); - }); - - // when in the menu - context(@"when in the menu", ^{ - beforeEach(^{ - [SDLGlobals sharedGlobals].rpcVersion = [SDLVersion versionWithString:@"6.0.0"]; - testManager.currentHMILevel = SDLHMILevelFull; - testManager.currentSystemContext = SDLSystemContextMenu; - }); - - it(@"should update the menu configuration", ^{ - testManager.menuConfiguration = testMenuConfiguration; - expect(mockConnectionManager.receivedRequests).toNot(beEmpty()); - expect(testManager.menuConfiguration).to(equal(testMenuConfiguration)); - }); - }); + expect(testManager.currentSystemContext).to(beNil()); + expect(testManager.currentMenuCells).to(beEmpty()); + expect(testManager.currentMenuConfiguration).to(beNil()); }); - // display capability updates - describe(@"display capability updates", ^{ + describe(@"when the manager stops", ^{ beforeEach(^{ - testManager.currentHMILevel = SDLHMILevelFull; - testManager.currentSystemContext = SDLSystemContextMain; + [testManager stop]; }); - it(@"should save the new window capability", ^{ - SDLWindowCapability *testWindowCapability = [[SDLWindowCapability alloc] init]; - testWindowCapability.textFields = @[[[SDLTextField alloc] initWithName:SDLTextFieldNameMenuName characterSet:SDLCharacterSetUtf8 width:500 rows:1]]; - OCMStub([mockSystemCapabilityManager defaultMainWindowCapability]).andReturn(testWindowCapability); - [testManager sdl_displayCapabilityDidUpdate]; + it(@"should reset correctly", ^{ + expect(testManager.menuCells).to(beEmpty()); - expect(testManager.windowCapability).to(equal(testWindowCapability)); + expect(@(testManager.dynamicMenuUpdatesMode)).to(equal(@(SDLDynamicMenuUpdatesModeOnWithCompatibility))); + expect(testManager.connectionManager).to(equal(mockConnectionManager)); + expect(testManager.fileManager).to(equal(mockFileManager)); + expect(testManager.systemCapabilityManager).to(equal(mockSystemCapabilityManager)); + expect(testManager.transactionQueue).toNot(beNil()); + expect(testManager.windowCapability).to(beNil()); + expect(testManager.currentHMILevel).to(beNil()); + expect(testManager.currentSystemContext).to(beNil()); + expect(testManager.currentMenuCells).to(beEmpty()); + expect(testManager.currentMenuConfiguration).to(beNil()); }); }); - // updating menu cells - describe(@"updating menu cells", ^{ + context(@"when in HMI NONE", ^{ beforeEach(^{ - testManager.currentHMILevel = SDLHMILevelFull; - testManager.currentSystemContext = SDLSystemContextMain; + SDLOnHMIStatus *noneStatus = [[SDLOnHMIStatus alloc] initWithHMILevel:SDLHMILevelNone systemContext:SDLSystemContextMain audioStreamingState:SDLAudioStreamingStateNotAudible videoStreamingState:nil windowID:nil]; + [[NSNotificationCenter defaultCenter] postNotification:[[SDLRPCNotificationNotification alloc] initWithName:SDLDidChangeHMIStatusNotification object:nil rpcNotification:noneStatus]]; }); - // HMI does not support a command secondary image - context(@"HMI does not support a command secondary image", ^{ - SDLArtwork *staticArtwork = [[SDLArtwork alloc] initWithStaticIcon:SDLStaticIconNameKey]; - - beforeEach(^{ - testManager.windowCapability.imageFields = @[commandIconField, submenuIconField, subMenuSecondaryArtworkField]; - textAndImageCell = [[SDLMenuCell alloc] initWithTitle:@"Test 2" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:staticArtwork voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; - textAndImageCell2 = [[SDLMenuCell alloc] initWithTitle:@"Test 3" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:staticArtwork submenuLayout:SDLMenuLayoutList subCells:@[textOnlyCell]]; - testManager.menuCells = @[textAndImageCell, textAndImageCell2]; - }); - - it(@"should not send secondaryArtwork in our request for addCommand but send it with addSubMenu", ^{ - SDLAddCommand *cellCommand = (SDLAddCommand *)testManager.inProgressUpdate.firstObject; - SDLAddSubMenu *cellSubMenu = (SDLAddSubMenu *)testManager.inProgressUpdate[1]; - expect(cellCommand.menuParams.menuName).to(equal(@"Test 2")); - expect(cellCommand.secondaryImage).to(beNil()); - expect(cellSubMenu.secondaryImage).toNot(beNil()); - }); + it(@"should not suspend the transaction queue", ^{ + expect(testManager.transactionQueue.isSuspended).to(beTrue()); }); - // HMI does not support a submenu secondary image - context(@"HMI does not support a submenu secondary image", ^{ - SDLArtwork *staticArtwork = [[SDLArtwork alloc] initWithStaticIcon:SDLStaticIconNameKey]; - + // when entering HMI FULL + describe(@"when entering HMI FULL", ^{ beforeEach(^{ - testManager.windowCapability.imageFields = @[commandIconField, submenuIconField, commandSecondaryArtworkField]; - textAndImageCell = [[SDLMenuCell alloc] initWithTitle:@"Test 2" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:staticArtwork submenuLayout:SDLMenuLayoutList subCells:@[textOnlyCell]]; - textAndImageCell2 = [[SDLMenuCell alloc] initWithTitle:@"Test 3" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:staticArtwork voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; - testManager.menuCells = @[textAndImageCell, textAndImageCell2]; - }); + SDLOnHMIStatus *onHMIStatus = [[SDLOnHMIStatus alloc] init]; + onHMIStatus.hmiLevel = SDLHMILevelFull; + onHMIStatus.systemContext = SDLSystemContextMain; - it(@"should not send secondaryArtwork in our request for addSubMenu but send it with addCommand", ^{ - SDLAddSubMenu *cellSubMenu = (SDLAddSubMenu *)testManager.inProgressUpdate.firstObject; - SDLAddCommand *cellCommand = (SDLAddCommand *)testManager.inProgressUpdate[1]; - expect(cellSubMenu.menuName).to(equal(@"Test 2")); - expect(cellSubMenu.secondaryImage).to(beNil()); - expect(cellCommand.secondaryImage).toNot(beNil()); - }); - }); - - // duplicate titles version >= 7.1.0 - context(@"duplicate titles version >= 7.1.0", ^{ - beforeEach(^{ - [SDLGlobals sharedGlobals].rpcVersion = menuUniquenessActiveVersion; - }); - - // if there are duplicate cells once you strip unused menu properties - context(@"if there are duplicate cells once you strip unused menu properties", ^{ - beforeEach(^{ - testManager.windowCapability.textFields = @[]; - testManager.windowCapability.imageFields = @[]; - }); - - it(@"should update the cells' unique title to include unique data", ^{ - 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 update subcells' unique title to include unique data", ^{ - 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)")); - }); + SDLRPCNotificationNotification *testSystemContextNotification = [[SDLRPCNotificationNotification alloc] initWithName:SDLDidChangeHMIStatusNotification object:nil rpcNotification:onHMIStatus]; + [[NSNotificationCenter defaultCenter] postNotification:testSystemContextNotification]; }); - // if there are no duplicate cells - context(@"if there are no duplicate cells", ^{ - 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")); - }); + it(@"should run the transaction queue", ^{ + expect(testManager.transactionQueue.isSuspended).to(beFalse()); }); }); + }); - // duplicate titles version <= 7.1.0 - 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 HMI is ready", ^{ + beforeEach(^{ + testManager.currentHMILevel = SDLHMILevelFull; + testManager.currentSystemContext = SDLSystemContextMain; }); - // when there are complete duplicates - describe(@"when there are complete duplicates", ^{ - // when the cells contain duplicates - 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]; + describe(@"setting new menu cells", ^{ + context(@"containing duplicate titles", ^{ + it(@"should not start an operation", ^{ + testManager.menuCells = @[textOnlyCell, textOnlyCell]; expect(testManager.menuCells).to(beEmpty()); + expect(testManager.transactionQueue.operationCount).to(equal(0)); }); }); - // when cells contain duplicate subcells - 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]]; + context(@"containing duplicate VR commands", ^{ + 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 3" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:@[@"Cat", @"Dog"] handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; - it(@"should fail with duplicate cells", ^{ - testManager.menuCells = @[textCell]; + it(@"should not start an operation", ^{ + testManager.menuCells = @[textAndVRCell1, textAndVRCell2]; expect(testManager.menuCells).to(beEmpty()); + expect(testManager.transactionQueue.operationCount).to(equal(0)); }); }); - context(@"duplicate VR commands", ^{ - __block SDLMenuCell *textAndVRCell1 = [[SDLMenuCell alloc] initWithTitle:@"Test 1" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:@[@"Cat", @"Turtle"] handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; - __block SDLMenuCell *textAndVRCell2 = [[SDLMenuCell alloc] initWithTitle:@"Test 3" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:@[@"Cat", @"Dog"] handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; + context(@"if the new menu cells are identical to the old menu cells", ^{ + it(@"should queue two transactions and let the operation handle not updating", ^{ + testManager.menuCells = @[textOnlyCell]; + testManager.menuCells = @[textOnlyCell]; - it(@"should fail when menu items have duplicate vr commands", ^{ - testManager.menuCells = @[textAndVRCell1, textAndVRCell2]; - expect(testManager.menuCells).to(beEmpty()); + expect(testManager.menuCells).to(equal(@[textOnlyCell])); + expect(testManager.transactionQueue.operationCount).to(equal(2)); }); }); - 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]]; + context(@"when a second menu cells update is queued before the first is done", ^{ + it(@"should cancel the first operation", ^{ + testManager.menuCells = @[textOnlyCell]; + testManager.menuCells = @[submenuCell]; - it(@"should fail when menu items have duplicate vr commands", ^{ - testManager.menuCells = @[textAndVRCell1, textAndVRCell2]; - expect(testManager.menuCells).to(beEmpty()); + expect(testManager.menuCells).to(equal(@[submenuCell])); + expect(testManager.transactionQueue.operationCount).to(equal(2)); + expect(testManager.transactionQueue.operations[0].isCancelled).to(beTrue()); }); }); - }); - // should check if all artworks are uploaded and return NO - 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]; - OCMVerify([testManager sdl_shouldRPCsIncludeImages:testManager.menuCells]); - expect([testManager sdl_shouldRPCsIncludeImages:testManager.menuCells]).to(beFalse()); - }); + context(@"if cells are formed properly", ^{ + it(@"should properly prepare and queue the transaction", ^{ + testManager.menuCells = @[textOnlyCell]; - // should properly update a text cell - it(@"should properly update a text cell", ^{ - testManager.menuCells = @[textOnlyCell]; + expect(testManager.transactionQueue.operationCount).to(equal(1)); + expect(testManager.transactionQueue.operations[0]).to(beAnInstanceOf([SDLMenuReplaceOperation class])); - NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLDeleteCommand class]]; - NSArray *deletes = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; - expect(deletes).to(beEmpty()); + // Assign proper current menu + SDLMenuReplaceOperation *testOp = testManager.transactionQueue.operations[0]; + expect(testOp.currentMenu).to(haveCount(0)); - NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddCommand class]]; - NSArray *add = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; - expect(add).toNot(beEmpty()); + // Callback proper current menu + testOp.currentMenu = @[textOnlyCell]; + [testOp finishOperation]; + expect(testManager.currentMenuCells).to(haveCount(1)); + }); + }); }); - // should properly update with subcells - it(@"should properly update with subcells", ^{ - OCMStub([mockFileManager uploadArtworks:[OCMArg any] completionHandler:[OCMArg invokeBlock]]); - testManager.menuCells = @[submenuCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - - NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddCommand class]]; - NSArray *adds = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; - - NSPredicate *submenuCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddSubMenu class]]; - NSArray *submenus = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:submenuCommandPredicate]; - - expect(adds).to(haveCount(2)); - expect(submenus).to(haveCount(1)); - }); + describe(@"updating the menu configuration", ^{ + beforeEach(^{ + testManager.currentHMILevel = SDLHMILevelFull; + testManager.currentSystemContext = SDLSystemContextMain; + }); - // updating with an image - describe(@"updating with an image", ^{ - context(@"when the image is already on the head unit", ^{ + context(@"if the connection RPC version is less than 6.0.0", ^{ beforeEach(^{ - OCMStub([mockFileManager hasUploadedFile:[OCMArg isNotNil]]).andReturn(YES); - }); - - it(@"should check if all artworks are uploaded", ^{ - textAndImageCell = [[SDLMenuCell alloc] initWithTitle:@"Test 2" secondaryText:nil tertiaryText:nil icon:testArtwork3 secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; - testManager.menuCells = @[textAndImageCell, textOnlyCell]; - OCMVerify([testManager sdl_shouldRPCsIncludeImages:testManager.menuCells]); - expect([testManager sdl_shouldRPCsIncludeImages:testManager.menuCells]).to(beTrue()); + [SDLGlobals sharedGlobals].rpcVersion = [SDLVersion versionWithString:@"5.0.0"]; }); - it(@"should properly update an image cell", ^{ - testManager.menuCells = @[textAndImageCell, submenuImageCell]; + it(@"should not queue a menu configuration update", ^{ + testManager.menuConfiguration = testMenuConfiguration; - NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddCommand class]]; - NSArray *add = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; - SDLAddCommand *sentCommand = add.firstObject; - - NSPredicate *addSubmenuPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddSubMenu class]]; - NSArray *submenu = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addSubmenuPredicate]; - SDLAddSubMenu *sentSubmenu = submenu.firstObject; - - expect(add).to(haveCount(1)); - expect(submenu).to(haveCount(1)); - expect(sentCommand.cmdIcon.value).to(equal(testArtwork.name)); - expect(sentSubmenu.menuIcon.value).to(equal(testArtwork2.name)); - OCMReject([mockFileManager uploadArtworks:[OCMArg any] completionHandler:[OCMArg any]]); - }); - - it(@"should properly overwrite an image cell", ^{ - OCMStub([mockFileManager fileNeedsUpload:[OCMArg isNotNil]]).andReturn(YES); - textAndImageCell = [[SDLMenuCell alloc] initWithTitle:@"Test 2" secondaryText:nil tertiaryText:nil icon:testArtwork3 secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; - testManager.menuCells = @[textAndImageCell, submenuImageCell]; - OCMVerify([mockFileManager uploadArtworks:[OCMArg any] completionHandler:[OCMArg any]]); + expect(testManager.menuConfiguration).toNot(equal(testMenuConfiguration)); + expect(testManager.transactionQueue.operationCount).to(equal(0)); }); }); - // No longer a valid unit test - context(@"when the image is not on the head unit", ^{ + context(@"if the connection RPC version is greater than or equal to 6.0.0", ^{ beforeEach(^{ - testManager.dynamicMenuUpdatesMode = SDLDynamicMenuUpdatesModeForceOff; - OCMStub([mockFileManager uploadArtworks:[OCMArg any] completionHandler:[OCMArg invokeBlock]]); + [SDLGlobals sharedGlobals].rpcVersion = [SDLVersion versionWithString:@"6.0.0"]; }); - it(@"should wait till image is on head unit and attempt to update without the image", ^{ - testManager.menuCells = @[textAndImageCell, submenuImageCell]; - - NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddCommand class]]; - NSArray *add = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; - SDLAddCommand *sentCommand = add.firstObject; - - NSPredicate *addSubmenuPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddSubMenu class]]; - NSArray *submenu = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addSubmenuPredicate]; - SDLAddSubMenu *sentSubmenu = submenu.firstObject; - - expect(add).to(haveCount(1)); - expect(submenu).to(haveCount(1)); - expect(sentCommand.cmdIcon.value).to(beNil()); - expect(sentSubmenu.menuIcon.value).to(beNil()); - }); - }); - }); - - describe(@"updating when a menu already exists with dynamic updates on", ^{ - beforeEach(^{ - testManager.dynamicMenuUpdatesMode = SDLDynamicMenuUpdatesModeForceOn; - OCMStub([mockFileManager uploadArtworks:[OCMArg any] completionHandler:[OCMArg invokeBlock]]); - }); - - it(@"should send deletes first", ^{ - testManager.menuCells = @[textOnlyCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - - testManager.menuCells = @[textAndImageCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - - NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; - NSArray *deletes = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; - - NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; - NSArray *adds = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; - - expect(deletes).to(haveCount(1)); - expect(adds).to(haveCount(2)); - }); - - it(@"should send dynamic deletes first then dynamic adds case with 2 submenu cells", ^{ - testManager.menuCells = @[textOnlyCell, submenuCell, submenuImageCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - - testManager.menuCells = @[submenuCell, submenuImageCell, textOnlyCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - - NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; - NSArray *deletes = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; - - NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; - NSArray *adds = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; - - NSPredicate *addSubmenuPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddSubMenu class]]; - NSArray *submenu = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addSubmenuPredicate]; - - expect(deletes).to(haveCount(1)); - expect(adds).to(haveCount(5)); - expect(submenu).to(haveCount(2)); - }); - - it(@"should send dynamic deletes first then dynamic adds when removing one submenu cell", ^{ - testManager.menuCells = @[textOnlyCell, textAndImageCell, submenuCell, submenuImageCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - - testManager.menuCells = @[textOnlyCell, textAndImageCell, submenuCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - - NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; - NSArray *deletes = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; - - NSPredicate *deleteSubCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteSubMenu class]]; - NSArray *subDeletes = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteSubCommandPredicate]; - - NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; - NSArray *adds = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; - - NSPredicate *addSubmenuPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddSubMenu class]]; - NSArray *submenu = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addSubmenuPredicate]; - - expect(deletes).to(haveCount(0)); - expect(subDeletes).to(haveCount(1)); - expect(adds).to(haveCount(5)); - expect(submenu).to(haveCount(2)); - }); - - it(@"should send dynamic deletes first then dynamic adds when adding one new cell", ^{ - testManager.menuCells = @[textOnlyCell, textAndImageCell, submenuCell, submenuImageCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + it(@"should should queue a menu configuration update", ^{ + testManager.menuConfiguration = testMenuConfiguration; - testManager.menuCells = @[textOnlyCell, textAndImageCell, submenuCell, submenuImageCell, textOnlyCell2]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - - NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; - NSArray *deletes = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; - - NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; - NSArray *adds = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; - - NSPredicate *addSubmenuPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddSubMenu class]]; - NSArray *submenu = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addSubmenuPredicate]; - - expect(deletes).to(haveCount(0)); - expect(adds).to(haveCount(6)); - expect(submenu).to(haveCount(2)); - }); - - it(@"should send dynamic deletes first then dynamic adds when cells stay the same", ^{ - testManager.menuCells = @[textOnlyCell, textOnlyCell2, textAndImageCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - - testManager.menuCells = @[textOnlyCell, textOnlyCell2, textAndImageCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - - NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; - NSArray *deletes = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; + expect(testManager.menuConfiguration).to(equal(testMenuConfiguration)); + expect(testManager.transactionQueue.operationCount).to(equal(1)); + }); - NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; - NSArray *adds = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + context(@"when queueing a second task after the first", ^{ + it(@"should cancel the first task", ^{ + testManager.menuConfiguration = testMenuConfiguration; + testManager.menuConfiguration = [[SDLMenuConfiguration alloc] initWithMainMenuLayout:SDLMenuLayoutList defaultSubmenuLayout:SDLMenuLayoutList]; - expect(deletes).to(haveCount(0)); - expect(adds).to(haveCount(3)); + expect(testManager.transactionQueue.operationCount).to(equal(2)); + expect(testManager.transactionQueue.operations[0].isCancelled).to(beTrue()); + }); + }); }); }); - describe(@"updating when a menu already exists with dynamic updates off", ^{ + describe(@"opening the menu", ^{ beforeEach(^{ - testManager.dynamicMenuUpdatesMode = SDLDynamicMenuUpdatesModeForceOff; - OCMStub([mockFileManager uploadArtworks:[OCMArg any] completionHandler:[OCMArg invokeBlock]]); - }); - - it(@"should send deletes first", ^{ - testManager.menuCells = @[textOnlyCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - - testManager.menuCells = @[textAndImageCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - - NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; - NSArray *deletes = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; - - NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; - NSArray *adds = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; - - expect(deletes).to(haveCount(1)); - expect(adds).to(haveCount(2)); - }); - - it(@"should deletes first case 2", ^{ - testManager.menuCells = @[textOnlyCell, textAndImageCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - - testManager.menuCells = @[textAndImageCell, textOnlyCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - - NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; - NSArray *deletes = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; - - NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; - NSArray *adds = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; - - expect(deletes).to(haveCount(2)); - expect(adds).to(haveCount(4)); + testManager.currentHMILevel = SDLHMILevelFull; + testManager.currentSystemContext = SDLSystemContextMain; }); - it(@"should send deletes first case 3", ^{ - testManager.menuCells = @[textOnlyCell, textAndImageCell, submenuCell, submenuImageCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + context(@"when open menu RPC can be sent", ^{ + beforeEach(^{ + SDLVersion *oldVersion = [SDLVersion versionWithMajor:6 minor:0 patch:0]; + id globalMock = OCMPartialMock([SDLGlobals sharedGlobals]); + OCMStub([globalMock rpcVersion]).andReturn(oldVersion); + }); - testManager.menuCells = @[textOnlyCell, textAndImageCell, submenuCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + // should queue an open menu operation for the main menu + it(@"should queue an open menu operation for the main menu", ^{ + BOOL canSendRPC = [testManager openMenu:nil]; - NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; - NSArray *deletes = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; + expect(testManager.transactionQueue.operationCount).to(equal(1)); + expect(canSendRPC).to(equal(YES)); + }); - NSPredicate *deleteSubCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteSubMenu class]]; - NSArray *subDeletes = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteSubCommandPredicate]; + // should queue an open menu operation for a submenu cell + it(@"should queue an open menu operation for a submenu cell", ^ { + testManager.menuCells = @[submenuCell]; + BOOL canSendRPC = [testManager openMenu:submenuCell]; - NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; - NSArray *adds = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + expect(testManager.transactionQueue.operationCount).to(equal(2)); + expect(canSendRPC).to(equal(YES)); + }); - NSPredicate *addSubmenuPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddSubMenu class]]; - NSArray *submenu = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addSubmenuPredicate]; + it(@"should cancel the first task if a second is queued", ^{ + testManager.menuCells = @[submenuCell]; + [testManager openMenu:nil]; + [testManager openMenu:submenuCell]; - expect(deletes).to(haveCount(2)); - expect(subDeletes).to(haveCount(2)); - expect(adds).to(haveCount(9)); - expect(submenu).to(haveCount(3)); + expect(testManager.transactionQueue.operationCount).to(equal(3)); + expect(testManager.transactionQueue.operations[1].isCancelled).to(beTrue()); + }); }); - it(@"should send deletes first case 4", ^{ - testManager.menuCells = @[textOnlyCell, textAndImageCell, submenuCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - - testManager.menuCells = @[textOnlyCell, textAndImageCell, submenuCell, textOnlyCell2]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - - - NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; - NSArray *deletes = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; - - NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; - NSArray *adds = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + context(@"when the open menu RPC can not be sent", ^{ + it(@"should not queue an open menu operation when cell has no subcells", ^ { + BOOL canSendRPC = [testManager openMenu:textOnlyCell]; - NSPredicate *addSubmenuPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddSubMenu class]]; - NSArray *submenu = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addSubmenuPredicate]; - - NSPredicate *deleteSubCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteSubMenu class]]; - NSArray *subDeletes = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteSubCommandPredicate]; + expect(testManager.transactionQueue.operationCount).to(equal(0)); + expect(canSendRPC).to(equal(NO)); + }); - expect(deletes).to(haveCount(2)); - expect(adds).to(haveCount(9)); - expect(submenu).to(haveCount(2)); - expect(subDeletes).to(haveCount(1)); - }); + it(@"should not queue an open menu operation when RPC version is not at least 6.0.0", ^ { + SDLVersion *oldVersion = [SDLVersion versionWithMajor:5 minor:0 patch:0]; + id globalMock = OCMPartialMock([SDLGlobals sharedGlobals]); + OCMStub([globalMock rpcVersion]).andReturn(oldVersion); - it(@"should deletes first case 5", ^{ - testManager.menuCells = @[textOnlyCell, textOnlyCell2, textAndImageCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + BOOL canSendRPC = [testManager openMenu:submenuCell]; - testManager.menuCells = @[textOnlyCell, textOnlyCell2, textAndImageCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + expect(testManager.transactionQueue.operationCount).to(equal(0)); + expect(canSendRPC).to(equal(NO)); + }); - NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; - NSArray *deletes = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; + it(@"should not queue an open menu operation when the cell is not in the menu array", ^ { + SDLVersion *oldVersion = [SDLVersion versionWithMajor:6 minor:0 patch:0]; + id globalMock = OCMPartialMock([SDLGlobals sharedGlobals]); + OCMStub([globalMock rpcVersion]).andReturn(oldVersion); - NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; - NSArray *adds = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + BOOL canSendRPC = [testManager openMenu:submenuCell]; - expect(deletes).to(haveCount(3)); - expect(adds).to(haveCount(6)); + expect(testManager.transactionQueue.operationCount).to(equal(0)); + expect(canSendRPC).to(equal(NO)); + }); }); }); }); @@ -725,8 +331,9 @@ - (void)sdl_displayCapabilityDidUpdate; cellCalled = YES; testTriggerSource = triggerSource; }]; + cellWithHandler.cellId = 1; - testManager.menuCells = @[cellWithHandler]; + testManager.currentMenuCells = @[cellWithHandler]; }); it(@"should call the cell handler", ^{ @@ -748,10 +355,14 @@ - (void)sdl_displayCapabilityDidUpdate; cellCalled = YES; testTriggerSource = triggerSource; }]; + cellWithHandler.cellId = 2; SDLMenuCell *submenuCell = [[SDLMenuCell alloc] initWithTitle:@"Submenu" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil submenuLayout:SDLMenuLayoutTiles subCells:@[cellWithHandler]]; + submenuCell.cellId = 1; + + cellWithHandler.parentCellId = 1; - testManager.menuCells = @[submenuCell]; + testManager.currentMenuCells = @[submenuCell]; }); it(@"should call the cell handler", ^{ @@ -767,154 +378,6 @@ - (void)sdl_displayCapabilityDidUpdate; }); }); }); - - describe(@"updating the menu configuration", ^{ - beforeEach(^{ - testManager.currentHMILevel = SDLHMILevelFull; - testManager.currentSystemContext = SDLSystemContextMain; - }); - - context(@"if the connection RPC version is less than 6.0.0", ^{ - beforeEach(^{ - [SDLGlobals sharedGlobals].rpcVersion = [SDLVersion versionWithString:@"5.0.0"]; - }); - - it(@"should fail to send a SetGlobalProperties RPC update", ^{ - testManager.menuConfiguration = testMenuConfiguration; - - expect(testManager.menuConfiguration).toNot(equal(testMenuConfiguration)); - expect(mockConnectionManager.receivedRequests).to(haveCount(0)); - }); - }); - - context(@"if the connection RPC version is greater than or equal to 6.0.0", ^{ - beforeEach(^{ - [SDLGlobals sharedGlobals].rpcVersion = [SDLVersion versionWithString:@"6.0.0"]; - }); - - it(@"should send a SetGlobalProperties RPC update", ^{ - testManager.menuConfiguration = testMenuConfiguration; - - expect(testManager.menuConfiguration).to(equal(testMenuConfiguration)); - expect(mockConnectionManager.receivedRequests).to(haveCount(1)); - - SDLSetGlobalPropertiesResponse *response = [[SDLSetGlobalPropertiesResponse alloc] init]; - response.success = @YES; - [mockConnectionManager respondToLastRequestWithResponse:response]; - - expect(testManager.menuConfiguration).to(equal(testMenuConfiguration)); - }); - }); - }); - - context(@"when the manager stops", ^{ - beforeEach(^{ - [testManager stop]; - }); - - it(@"should reset correctly", ^{ - expect(testManager.connectionManager).to(equal(mockConnectionManager)); - expect(testManager.fileManager).to(equal(mockFileManager)); - - expect(testManager.menuCells).to(beEmpty()); - expect(testManager.currentHMILevel).to(beNil()); - expect(testManager.inProgressUpdate).to(beNil()); - expect(testManager.hasQueuedUpdate).to(beFalse()); - expect(testManager.waitingOnHMIUpdate).to(beFalse()); - expect(testManager.lastMenuId).to(equal(1)); - expect(testManager.oldMenuCells).to(beEmpty()); - expect(testManager.waitingUpdateMenuCells).to(beEmpty()); - expect(testManager.menuConfiguration).toNot(beNil()); - }); - }); - - describe(@"ShowMenu RPC", ^{ - beforeEach(^{ - testManager.currentHMILevel = SDLHMILevelFull; - testManager.currentSystemContext = SDLSystemContextMain; - }); - - context(@"when open menu RPC can be sent", ^{ - beforeEach(^{ - SDLVersion *oldVersion = [SDLVersion versionWithMajor:6 minor:0 patch:0]; - id globalMock = OCMPartialMock([SDLGlobals sharedGlobals]); - OCMStub([globalMock rpcVersion]).andReturn(oldVersion); - }); - - it(@"should send showAppMenu RPC", ^{ - BOOL canSendRPC = [testManager openMenu]; - - NSPredicate *showMenu = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLShowAppMenu class]]; - NSArray *openMenu = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:showMenu]; - - expect(mockConnectionManager.receivedRequests).toNot(beEmpty()); - expect(openMenu).to(haveCount(1)); - expect(canSendRPC).to(equal(YES)); - }); - - it(@"should send showAppMenu RPC with cellID", ^ { - testManager.menuCells = @[submenuCell]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; - - BOOL canSendRPC = [testManager openSubmenu:submenuCell]; - - NSPredicate *addSubmenuPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLShowAppMenu class]]; - NSArray *openMenu = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addSubmenuPredicate]; - - expect(mockConnectionManager.receivedRequests).toNot(beEmpty()); - expect(openMenu).to(haveCount(1)); - expect(canSendRPC).to(equal(YES)); - }); - }); - - context(@"when open menu RPC can not be sent", ^{ - it(@"should not send a showAppMenu RPC when cell has no subcells", ^ { - BOOL canSendRPC = [testManager openSubmenu:textOnlyCell]; - - NSPredicate *addSubmenuPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLShowAppMenu class]]; - NSArray *openMenu = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addSubmenuPredicate]; - - expect(mockConnectionManager.receivedRequests).to(beEmpty()); - expect(openMenu).to(haveCount(0)); - expect(canSendRPC).to(equal(NO)); - }); - - it(@"should not send a showAppMenu RPC when RPC verison is not at least 6.0.0", ^ { - SDLVersion *oldVersion = [SDLVersion versionWithMajor:5 minor:0 patch:0]; - id globalMock = OCMPartialMock([SDLGlobals sharedGlobals]); - OCMStub([globalMock rpcVersion]).andReturn(oldVersion); - - BOOL canSendRPC = [testManager openSubmenu:submenuCell]; - - NSPredicate *addSubmenuPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLShowAppMenu class]]; - NSArray *openMenu = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addSubmenuPredicate]; - - expect(mockConnectionManager.receivedRequests).to(beEmpty()); - expect(openMenu).to(haveCount(0)); - expect(canSendRPC).to(equal(NO)); - }); - - it(@"should not send a showAppMenu RPC when the cell is not in the menu array", ^ { - SDLVersion *oldVersion = [SDLVersion versionWithMajor:6 minor:0 patch:0]; - id globalMock = OCMPartialMock([SDLGlobals sharedGlobals]); - OCMStub([globalMock rpcVersion]).andReturn(oldVersion); - - BOOL canSendRPC = [testManager openSubmenu:submenuCell]; - - NSPredicate *addSubmenuPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLShowAppMenu class]]; - NSArray *openMenu = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addSubmenuPredicate]; - - expect(mockConnectionManager.receivedRequests).to(beEmpty()); - expect(openMenu).to(haveCount(0)); - expect(canSendRPC).to(equal(NO)); - }); - }); - }); - - afterEach(^{ - testManager = nil; - }); }); QuickSpecEnd diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLMenuReplaceOperationSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuReplaceOperationSpec.m new file mode 100644 index 000000000..bb3c662bb --- /dev/null +++ b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuReplaceOperationSpec.m @@ -0,0 +1,745 @@ +// +// SDLMenuReplaceOperationSpec.m +// SmartDeviceLinkTests +// +// Created by Joel Fischer on 2/16/21. +// Copyright © 2021 smartdevicelink. All rights reserved. +// + +#import +#import +#import + +#import +#import "SDLGlobals.h" +#import "SDLMenuReplaceOperation.h" +#import "SDLMenuManagerPrivateConstants.h" +#import "SDLMenuReplaceUtilities.h" +#import "TestConnectionManager.h" + +@interface SDLMenuCell () + +@property (assign, nonatomic) UInt32 parentCellId; +@property (assign, nonatomic) UInt32 cellId; +@property (strong, nonatomic, readwrite) NSString *uniqueTitle; + +@property (copy, nonatomic, readwrite) NSString *title; +@property (strong, nonatomic, readwrite, nullable) SDLArtwork *icon; +@property (copy, nonatomic, readwrite, nullable) NSArray *voiceCommands; +@property (copy, nonatomic, readwrite, nullable) NSString *secondaryText; +@property (copy, nonatomic, readwrite, nullable) NSString *tertiaryText; +@property (strong, nonatomic, readwrite, nullable) SDLArtwork *secondaryArtwork; +@property (copy, nonatomic, readwrite, nullable) NSArray *subCells; +@property (copy, nonatomic, readwrite, nullable) SDLMenuCellSelectionHandler handler; + +@end + +QuickSpecBegin(SDLMenuReplaceOperationSpec) + +describe(@"a menu replace operation", ^{ + __block SDLMenuReplaceOperation *testOp = nil; + + __block TestConnectionManager *testConnectionManager = nil; + __block SDLFileManager *testFileManager = nil; + __block SDLMenuConfiguration *testMenuConfiguration = nil; + __block NSArray *testCurrentMenu = nil; + __block NSArray *testNewMenu = nil; + + SDLTextField *commandSecondaryTextField = [[SDLTextField alloc] initWithName:SDLTextFieldNameMenuCommandSecondaryText characterSet:SDLCharacterSetUtf8 width:200 rows:1]; + SDLTextField *commandTertiaryTextField = [[SDLTextField alloc] initWithName:SDLTextFieldNameMenuCommandTertiaryText characterSet:SDLCharacterSetUtf8 width:200 rows:1]; + SDLTextField *submenuSecondaryTextField = [[SDLTextField alloc] initWithName:SDLTextFieldNameMenuSubMenuSecondaryText characterSet:SDLCharacterSetUtf8 width:200 rows:1]; + SDLTextField *submenuTertiaryTextField = [[SDLTextField alloc] initWithName:SDLTextFieldNameMenuSubMenuTertiaryText characterSet:SDLCharacterSetUtf8 width:200 rows:1]; + SDLImageField *commandImageField = [[SDLImageField alloc] initWithName:SDLImageFieldNameCommandIcon imageTypeSupported:@[SDLFileTypePNG] imageResolution:nil]; + SDLImageField *submenuImageField = [[SDLImageField alloc] initWithName:SDLImageFieldNameSubMenuIcon imageTypeSupported:@[SDLFileTypePNG] imageResolution:nil]; + __block SDLWindowCapability *testWindowCapability = [[SDLWindowCapability alloc] initWithWindowID:@0 textFields:@[commandSecondaryTextField, commandTertiaryTextField, submenuSecondaryTextField, submenuTertiaryTextField] imageFields:@[commandImageField, submenuImageField] imageTypeSupported:nil templatesAvailable:nil numCustomPresetsAvailable:nil buttonCapabilities:nil softButtonCapabilities:nil menuLayoutsAvailable:nil dynamicUpdateCapabilities:nil keyboardCapabilities:nil]; + __block SDLWindowCapability *testTitleOnlyWindowCapability = [[SDLWindowCapability alloc] initWithWindowID:@0 textFields:@[] imageFields:@[commandImageField, submenuImageField] imageTypeSupported:nil templatesAvailable:nil numCustomPresetsAvailable:nil buttonCapabilities:nil softButtonCapabilities:nil menuLayoutsAvailable:nil dynamicUpdateCapabilities:nil keyboardCapabilities:nil]; + + __block SDLArtwork *testArtwork = nil; + __block SDLArtwork *testArtwork2 = nil; + __block SDLArtwork *testArtwork3 = nil; + + __block SDLMenuCell *textOnlyCell = nil; + __block SDLMenuCell *textOnlyCell2 = nil; + __block SDLMenuCell *textAndImageCell = nil; + __block SDLMenuCell *submenuCell = nil; + __block SDLMenuCell *submenuCellReversed = nil; + __block SDLMenuCell *submenuImageCell = nil; + + __block SDLAddCommandResponse *addCommandSuccessResponse = nil; + __block SDLAddSubMenuResponse *addSubMenuSuccessResponse = nil; + __block SDLDeleteCommandResponse *deleteCommandSuccessResponse = nil; + __block SDLDeleteSubMenuResponse *deleteSubMenuSuccessResponse = nil; + + __block NSMutableArray *basicCellArray = [NSMutableArray array]; + + __block NSArray *resultMenuCells = nil; + __block NSError *resultError = nil; + __block SDLCurrentMenuUpdatedBlock testCurrentMenuUpdatedBlock = nil; + + __block SDLMenuReplaceUtilities *mockReplaceUtilities = nil; + + beforeSuite(^{ + for (int i = 0; i < 50; i++) { + NSString *cellTitle = [NSString stringWithFormat:@"Cell %@", @(i)]; + [basicCellArray addObject:[[SDLMenuCell alloc] initWithTitle:cellTitle secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:@[cellTitle] handler:^(SDLTriggerSource _Nonnull triggerSource) { + NSLog(@"%@ pressed", cellTitle); + }]]; + } + }); + + beforeEach(^{ + [SDLGlobals sharedGlobals].rpcVersion = [SDLVersion versionWithMajor:7 minor:1 patch:0]; + + 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]; + testArtwork3 = [[SDLArtwork alloc] initWithData:[@"Test data 3" dataUsingEncoding:NSUTF8StringEncoding] name:@"some artwork name" fileExtension:@"png" persistent:NO]; + testArtwork3.overwrite = YES; + + textOnlyCell = [[SDLMenuCell alloc] initWithTitle:@"Test 1" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; + textOnlyCell2 = [[SDLMenuCell alloc] initWithTitle:@"Test 5" 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) {}]; + submenuCell = [[SDLMenuCell alloc] initWithTitle:@"Cell with Subcells" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil submenuLayout:nil subCells:basicCellArray]; + submenuCellReversed = [[SDLMenuCell alloc] initWithTitle:@"Cell with Subcells" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil submenuLayout:nil subCells:basicCellArray.reverseObjectEnumerator.allObjects]; + submenuImageCell = [[SDLMenuCell alloc] initWithTitle:@"Cell with Image and Subcell" secondaryText:nil tertiaryText:nil icon:testArtwork2 secondaryArtwork:nil submenuLayout:SDLMenuLayoutTiles subCells:@[textOnlyCell]]; + + addCommandSuccessResponse = [[SDLAddCommandResponse alloc] init]; + addCommandSuccessResponse.success = @YES; + addCommandSuccessResponse.resultCode = SDLResultSuccess; + addSubMenuSuccessResponse = [[SDLAddSubMenuResponse alloc] init]; + addSubMenuSuccessResponse.success = @YES; + addSubMenuSuccessResponse.resultCode = SDLResultSuccess; + deleteCommandSuccessResponse = [[SDLDeleteCommandResponse alloc] init]; + deleteCommandSuccessResponse.success = @YES; + deleteCommandSuccessResponse.resultCode = SDLResultSuccess; + deleteSubMenuSuccessResponse = [[SDLDeleteSubMenuResponse alloc] init]; + deleteSubMenuSuccessResponse.success = @YES; + deleteSubMenuSuccessResponse.resultCode = SDLResultSuccess; + + testOp = nil; + testConnectionManager = [[TestConnectionManager alloc] init]; + testFileManager = OCMClassMock([SDLFileManager class]); + + testMenuConfiguration = [[SDLMenuConfiguration alloc] initWithMainMenuLayout:SDLMenuLayoutList defaultSubmenuLayout:SDLMenuLayoutList]; + testCurrentMenu = @[]; + testNewMenu = nil; + + resultMenuCells = nil; + resultError = nil; + testCurrentMenuUpdatedBlock = ^(NSArray *currentMenuCells, NSError *error) { + resultMenuCells = currentMenuCells; + resultError = error; + }; + + mockReplaceUtilities = OCMClassMock([SDLMenuReplaceUtilities class]); + }); + + context(@"sending initial batch of cells", ^{ + context(@"when setting no cells", ^{ + it(@"should finish without doing anything", ^{ + testOp = [[SDLMenuReplaceOperation alloc] initWithConnectionManager:testConnectionManager fileManager:testFileManager windowCapability:testWindowCapability menuConfiguration:testMenuConfiguration currentMenu:testCurrentMenu updatedMenu:testNewMenu compatibilityModeEnabled:YES currentMenuUpdatedHandler:testCurrentMenuUpdatedBlock]; + [testOp start]; + + expect(testConnectionManager.receivedRequests).to(beEmpty()); + expect(testOp.isFinished).to(beTrue()); + expect(resultError).to(beNil()); + expect(resultMenuCells).to(beEmpty()); + }); + }); + + context(@"when starting while cancelled", ^{ + it(@"should finish without doing anything", ^{ + testOp = [[SDLMenuReplaceOperation alloc] initWithConnectionManager:testConnectionManager fileManager:testFileManager windowCapability:testWindowCapability menuConfiguration:testMenuConfiguration currentMenu:testCurrentMenu updatedMenu:testNewMenu compatibilityModeEnabled:YES currentMenuUpdatedHandler:testCurrentMenuUpdatedBlock]; + [testOp cancel]; + [testOp start]; + + expect(testConnectionManager.receivedRequests).to(beEmpty()); + expect(testOp.isFinished).to(beTrue()); + expect(resultError).to(beNil()); + expect(resultMenuCells).to(beNil()); + }); + }); + + context(@"when uploading a text-only cell", ^{ + beforeEach(^{ + testNewMenu = @[textOnlyCell]; + OCMStub([testFileManager fileNeedsUpload:[OCMArg any]]).andReturn(NO); + }); + + it(@"should properly send the RPCs and finish the operation", ^{ + testOp = [[SDLMenuReplaceOperation alloc] initWithConnectionManager:testConnectionManager fileManager:testFileManager windowCapability:testWindowCapability menuConfiguration:testMenuConfiguration currentMenu:testCurrentMenu updatedMenu:testNewMenu compatibilityModeEnabled:YES currentMenuUpdatedHandler:testCurrentMenuUpdatedBlock]; + [testOp start]; + + OCMReject([testFileManager uploadArtworks:[OCMArg any] progressHandler:[OCMArg any] completionHandler:[OCMArg any]]); + + NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLDeleteCommand class]]; + NSArray *deletes = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; + expect(deletes).to(beEmpty()); + + NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddCommand class]]; + NSArray *add = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + expect(add).to(haveCount(1)); + + [testConnectionManager respondToLastRequestWithResponse:addCommandSuccessResponse]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + + expect(testOp.isFinished).to(beTrue()); + expect(resultError).to(beNil()); + expect(resultMenuCells).to(haveCount(1)); + }); + }); + + context(@"when uploading text and image cell", ^{ + beforeEach(^{ + testNewMenu = @[textAndImageCell]; + + OCMStub([testFileManager uploadArtworks:[OCMArg any] progressHandler:([OCMArg invokeBlockWithArgs:textAndImageCell.icon.name, @1.0, [NSNull null], nil]) completionHandler:([OCMArg invokeBlockWithArgs: @[textAndImageCell.icon.name], [NSNull null], nil])]); + }); + + // when the image is already on the head unit + context(@"when the image is already on the head unit", ^{ + beforeEach(^{ + OCMStub([testFileManager hasUploadedFile:[OCMArg isNotNil]]).andReturn(YES); + OCMStub([testFileManager fileNeedsUpload:[OCMArg isNotNil]]).andReturn(NO); + }); + + it(@"should properly update an image cell", ^{ + OCMReject([testFileManager uploadArtworks:[OCMArg any] progressHandler:[OCMArg any] completionHandler:[OCMArg any]]); + + + testOp = [[SDLMenuReplaceOperation alloc] initWithConnectionManager:testConnectionManager fileManager:testFileManager windowCapability:testWindowCapability menuConfiguration:testMenuConfiguration currentMenu:testCurrentMenu updatedMenu:testNewMenu compatibilityModeEnabled:YES currentMenuUpdatedHandler:testCurrentMenuUpdatedBlock]; + [testOp start]; + + NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddCommand class]]; + NSArray *add = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + SDLAddCommand *sentCommand = add.firstObject; + + expect(add).to(haveCount(1)); + expect(sentCommand.cmdIcon.value).to(equal(testArtwork.name)); + + [testConnectionManager respondToLastRequestWithResponse:addCommandSuccessResponse]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + + expect(testOp.isFinished).to(beTrue()); + expect(resultError).to(beNil()); + expect(resultMenuCells).to(haveCount(1)); + }); + }); + + // when the image is not on the head unit + context(@"when the image is not on the head unit", ^{ + beforeEach(^{ + OCMStub([testFileManager hasUploadedFile:[OCMArg isNotNil]]).andReturn(NO); + OCMStub([testFileManager fileNeedsUpload:[OCMArg isNotNil]]).andReturn(YES); + }); + + it(@"should attempt to upload artworks then send the add", ^{ + testOp = [[SDLMenuReplaceOperation alloc] initWithConnectionManager:testConnectionManager fileManager:testFileManager windowCapability:testWindowCapability menuConfiguration:testMenuConfiguration currentMenu:testCurrentMenu updatedMenu:testNewMenu compatibilityModeEnabled:YES currentMenuUpdatedHandler:testCurrentMenuUpdatedBlock]; + [testOp start]; + + OCMVerify([testFileManager uploadArtworks:[OCMArg any] progressHandler:[OCMArg any] completionHandler:[OCMArg any]]); + + NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLDeleteCommand class]]; + NSArray *deletesArray = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; + expect(deletesArray).to(beEmpty()); + + NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddCommand class]]; + NSArray *addsArray = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + expect(addsArray).toNot(beEmpty()); + + [testConnectionManager respondToLastRequestWithResponse:addCommandSuccessResponse]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + + expect(testOp.isFinished).to(beTrue()); + expect(resultError).to(beNil()); + expect(resultMenuCells).to(haveCount(1)); + }); + }); + }); + + context(@"when uploading a cell with subcells", ^{ + beforeEach(^{ + testNewMenu = @[submenuCell]; + }); + + it(@"should send an appropriate number of AddSubmenu and AddCommandRequests", ^{ + testOp = [[SDLMenuReplaceOperation alloc] initWithConnectionManager:testConnectionManager fileManager:testFileManager windowCapability:testWindowCapability menuConfiguration:testMenuConfiguration currentMenu:testCurrentMenu updatedMenu:testNewMenu compatibilityModeEnabled:YES currentMenuUpdatedHandler:testCurrentMenuUpdatedBlock]; + [testOp start]; + + [testConnectionManager respondToLastRequestWithResponse:addSubMenuSuccessResponse]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + + NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddCommand class]]; + NSArray *adds = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + + NSPredicate *submenuCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddSubMenu class]]; + NSArray *submenus = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:submenuCommandPredicate]; + + expect(adds).to(haveCount(50)); + expect(submenus).to(haveCount(1)); + + [testConnectionManager respondToRequestWithResponse:addCommandSuccessResponse requestNumber:1 error:nil]; + [testConnectionManager respondToRequestWithResponse:addCommandSuccessResponse requestNumber:2 error:nil]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + + expect(testOp.isFinished).to(beTrue()); + expect(resultError).to(beNil()); + expect(resultMenuCells).to(haveCount(1)); + expect(resultMenuCells[0].subCells).to(haveCount(2)); + }); + }); + }); + + context(@"updating a menu without dynamic updates", ^{ + describe(@"basic cell updates", ^{ + context(@"adding a text cell", ^{ + beforeEach(^{ + testCurrentMenu = [[NSArray alloc] initWithArray:@[textOnlyCell] copyItems:YES]; + [SDLMenuReplaceUtilities addIdsToMenuCells:testCurrentMenu parentId:ParentIdNotFound]; + + testNewMenu = [[NSArray alloc] initWithArray:@[textOnlyCell, textOnlyCell2] copyItems:YES]; + }); + + it(@"should send a delete and two adds", ^{ + testOp = [[SDLMenuReplaceOperation alloc] initWithConnectionManager:testConnectionManager fileManager:testFileManager windowCapability:testWindowCapability menuConfiguration:testMenuConfiguration currentMenu:testCurrentMenu updatedMenu:testNewMenu compatibilityModeEnabled:YES currentMenuUpdatedHandler:testCurrentMenuUpdatedBlock]; + [testOp start]; + + [testConnectionManager respondToLastRequestWithResponse:deleteCommandSuccessResponse]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + + [testConnectionManager respondToRequestWithResponse:addCommandSuccessResponse requestNumber:1 error:nil]; + [testConnectionManager respondToRequestWithResponse:addCommandSuccessResponse requestNumber:2 error:nil]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + + NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; + NSArray *deletes = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; + + NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; + NSArray *adds = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + + expect(deletes).to(haveCount(1)); + expect(adds).to(haveCount(2)); + + expect(testOp.isFinished).to(beTrue()); + expect(resultError).to(beNil()); + expect(resultMenuCells).to(haveCount(2)); + expect(resultMenuCells[0]).to(equal(textOnlyCell)); + expect(resultMenuCells[1]).to(equal(textOnlyCell2)); + }); + }); + + context(@"when all cells remain the same", ^{ + beforeEach(^{ + testCurrentMenu = [[NSArray alloc] initWithArray:@[textOnlyCell, textOnlyCell2, textAndImageCell] copyItems:YES]; + [SDLMenuReplaceUtilities addIdsToMenuCells:testCurrentMenu parentId:ParentIdNotFound]; + + testNewMenu = [[NSArray alloc] initWithArray:@[textOnlyCell, textOnlyCell2, textAndImageCell] copyItems:YES]; + }); + + it(@"should delete all cells and add the new ones", ^{ + testOp = [[SDLMenuReplaceOperation alloc] initWithConnectionManager:testConnectionManager fileManager:testFileManager windowCapability:testWindowCapability menuConfiguration:testMenuConfiguration currentMenu:testCurrentMenu updatedMenu:testNewMenu compatibilityModeEnabled:YES currentMenuUpdatedHandler:testCurrentMenuUpdatedBlock]; + [testOp start]; + + [testConnectionManager respondToRequestWithResponse:deleteCommandSuccessResponse requestNumber:0 error:nil]; + [testConnectionManager respondToRequestWithResponse:deleteCommandSuccessResponse requestNumber:1 error:nil]; + [testConnectionManager respondToRequestWithResponse:deleteCommandSuccessResponse requestNumber:2 error:nil]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + + [testConnectionManager respondToRequestWithResponse:addCommandSuccessResponse requestNumber:3 error:nil]; + [testConnectionManager respondToRequestWithResponse:addCommandSuccessResponse requestNumber:4 error:nil]; + [testConnectionManager respondToRequestWithResponse:addCommandSuccessResponse requestNumber:5 error:nil]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + + NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; + NSArray *deletes = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; + + NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; + NSArray *adds = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + + expect(deletes).to(haveCount(3)); + expect(adds).to(haveCount(3)); + + expect(testOp.isFinished).to(beTrue()); + expect(resultError).to(beNil()); + expect(resultMenuCells).to(haveCount(3)); + }); + }); + }); + + describe(@"unique cell updates", ^{ + context(@"with cell uniqueness", ^{ + beforeEach(^{ + [SDLGlobals sharedGlobals].rpcVersion = [SDLVersion versionWithMajor:7 minor:1 patch:0]; + }); + + context(@"when cells have the same title but are unique", ^{ + beforeEach(^{ + testCurrentMenu = @[]; + + SDLMenuCell *textOnlyCellDupe = [textOnlyCell copy]; + textOnlyCellDupe.secondaryText = @"Secondary Text"; + + testNewMenu = @[textOnlyCell, textOnlyCellDupe]; + }); + + it(@"should send the cells unchanged", ^{ + testOp = [[SDLMenuReplaceOperation alloc] initWithConnectionManager:testConnectionManager fileManager:testFileManager windowCapability:testWindowCapability menuConfiguration:testMenuConfiguration currentMenu:testCurrentMenu updatedMenu:testNewMenu compatibilityModeEnabled:NO currentMenuUpdatedHandler:testCurrentMenuUpdatedBlock]; + [testOp start]; + + NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; + NSArray *deletes = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; + + NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; + NSArray *adds = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + + expect(deletes).to(haveCount(0)); + expect(adds).to(haveCount(2)); + + [testConnectionManager respondToRequestWithResponse:addCommandSuccessResponse requestNumber:0 error:nil]; + [testConnectionManager respondToRequestWithResponse:addCommandSuccessResponse requestNumber:1 error:nil]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + + expect(testOp.isFinished).to(beTrue()); + expect(resultError).to(beNil()); + expect(resultMenuCells).to(haveCount(2)); + expect(resultMenuCells[0].uniqueTitle).to(equal(textOnlyCell.uniqueTitle)); + expect(resultMenuCells[0].secondaryText).to(beNil()); + expect(resultMenuCells[1].uniqueTitle).to(equal(textOnlyCell.uniqueTitle)); + expect(resultMenuCells[1].secondaryText).toNot(beNil()); + }); + }); + + context(@"when cells are unique but are identical when stripped", ^{ + beforeEach(^{ + testCurrentMenu = @[]; + + SDLMenuCell *textOnlyCellDupe = [textOnlyCell copy]; + textOnlyCellDupe.secondaryText = @"Secondary Text"; + + testNewMenu = [[NSArray alloc] initWithArray:@[textOnlyCell, textOnlyCellDupe] copyItems:YES]; + }); + + it(@"should change the second cell's title", ^{ + testOp = [[SDLMenuReplaceOperation alloc] initWithConnectionManager:testConnectionManager fileManager:testFileManager windowCapability:testTitleOnlyWindowCapability menuConfiguration:testMenuConfiguration currentMenu:testCurrentMenu updatedMenu:testNewMenu compatibilityModeEnabled:NO currentMenuUpdatedHandler:testCurrentMenuUpdatedBlock]; + [testOp start]; + + NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; + NSArray *deletes = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; + + NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; + NSArray *adds = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + + expect(deletes).to(haveCount(0)); + expect(adds).to(haveCount(2)); + + [testConnectionManager respondToRequestWithResponse:addCommandSuccessResponse requestNumber:0 error:nil]; + [testConnectionManager respondToRequestWithResponse:addCommandSuccessResponse requestNumber:1 error:nil]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + + expect(testOp.isFinished).to(beTrue()); + expect(resultError).to(beNil()); + expect(resultMenuCells).to(haveCount(2)); + expect(resultMenuCells[0].uniqueTitle).to(equal(textOnlyCell.uniqueTitle)); + expect(resultMenuCells[0].secondaryText).to(beNil()); + expect(resultMenuCells[1].uniqueTitle).toNot(equal(textOnlyCell.uniqueTitle)); + expect(resultMenuCells[1].secondaryText).toNot(beNil()); + }); + }); + }); + + context(@"without cell uniqueness", ^{ + beforeEach(^{ + [SDLGlobals sharedGlobals].rpcVersion = [SDLVersion versionWithMajor:7 minor:0 patch:0]; + }); + + context(@"when cells have the same title but are unique", ^{ + beforeEach(^{ + testCurrentMenu = @[]; + + SDLMenuCell *textOnlyCellDupe = [textOnlyCell copy]; + textOnlyCellDupe.secondaryText = @"Secondary Text"; + + testNewMenu = @[textOnlyCell, textOnlyCellDupe]; + }); + + it(@"should change the second cell's title", ^{ + testOp = [[SDLMenuReplaceOperation alloc] initWithConnectionManager:testConnectionManager fileManager:testFileManager windowCapability:testWindowCapability menuConfiguration:testMenuConfiguration currentMenu:testCurrentMenu updatedMenu:testNewMenu compatibilityModeEnabled:NO currentMenuUpdatedHandler:testCurrentMenuUpdatedBlock]; + [testOp start]; + + NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; + NSArray *deletes = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; + + NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; + NSArray *adds = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + + expect(deletes).to(haveCount(0)); + expect(adds).to(haveCount(2)); + + [testConnectionManager respondToRequestWithResponse:addCommandSuccessResponse requestNumber:0 error:nil]; + [testConnectionManager respondToRequestWithResponse:addCommandSuccessResponse requestNumber:1 error:nil]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + + expect(testOp.isFinished).to(beTrue()); + expect(resultError).to(beNil()); + expect(resultMenuCells).to(haveCount(2)); + expect(resultMenuCells[0].uniqueTitle).to(equal(textOnlyCell.uniqueTitle)); + expect(resultMenuCells[0].secondaryText).to(beNil()); + expect(resultMenuCells[1].uniqueTitle).toNot(equal(textOnlyCell.uniqueTitle)); + expect(resultMenuCells[1].secondaryText).toNot(beNil()); + }); + }); + + context(@"when cells are unique but are identical when stripped", ^{ + beforeEach(^{ + testCurrentMenu = @[]; + + SDLMenuCell *textOnlyCellDupe = [textOnlyCell copy]; + textOnlyCellDupe.secondaryText = @"Secondary Text"; + + testNewMenu = [[NSArray alloc] initWithArray:@[textOnlyCell, textOnlyCellDupe] copyItems:YES]; + }); + + it(@"should change the second cell's title", ^{ + testOp = [[SDLMenuReplaceOperation alloc] initWithConnectionManager:testConnectionManager fileManager:testFileManager windowCapability:testTitleOnlyWindowCapability menuConfiguration:testMenuConfiguration currentMenu:testCurrentMenu updatedMenu:testNewMenu compatibilityModeEnabled:NO currentMenuUpdatedHandler:testCurrentMenuUpdatedBlock]; + [testOp start]; + + NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; + NSArray *deletes = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; + + NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; + NSArray *adds = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + + expect(deletes).to(haveCount(0)); + expect(adds).to(haveCount(2)); + + [testConnectionManager respondToRequestWithResponse:addCommandSuccessResponse requestNumber:0 error:nil]; + [testConnectionManager respondToRequestWithResponse:addCommandSuccessResponse requestNumber:1 error:nil]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + + expect(testOp.isFinished).to(beTrue()); + expect(resultError).to(beNil()); + expect(resultMenuCells).to(haveCount(2)); + expect(resultMenuCells[0].uniqueTitle).to(equal(textOnlyCell.uniqueTitle)); + expect(resultMenuCells[0].secondaryText).to(beNil()); + expect(resultMenuCells[1].uniqueTitle).toNot(equal(textOnlyCell.uniqueTitle)); + expect(resultMenuCells[1].secondaryText).toNot(beNil()); + }); + }); + }); + }); + }); + + context(@"updating a menu with dynamic updates", ^{ + context(@"adding a text cell", ^{ + beforeEach(^{ + testCurrentMenu = @[textOnlyCell]; + testNewMenu = @[textOnlyCell, textOnlyCell2]; + }); + + it(@"should only send an add", ^{ + testOp = [[SDLMenuReplaceOperation alloc] initWithConnectionManager:testConnectionManager fileManager:testFileManager windowCapability:testWindowCapability menuConfiguration:testMenuConfiguration currentMenu:testCurrentMenu updatedMenu:testNewMenu compatibilityModeEnabled:NO currentMenuUpdatedHandler:testCurrentMenuUpdatedBlock]; + [testOp start]; + + NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; + NSArray *deletes = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; + + NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; + NSArray *adds = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + + expect(deletes).to(haveCount(0)); + expect(adds).to(haveCount(1)); + + [testConnectionManager respondToLastRequestWithResponse:addCommandSuccessResponse]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + + expect(testOp.isFinished).to(beTrue()); + expect(resultError).to(beNil()); + expect(resultMenuCells).to(haveCount(2)); + }); + }); + + context(@"rearranging cells with subcells", ^{ + beforeEach(^{ + testCurrentMenu = [[NSArray alloc] initWithArray:@[textOnlyCell, submenuCell, submenuImageCell] copyItems:YES]; + [SDLMenuReplaceUtilities addIdsToMenuCells:testCurrentMenu parentId:ParentIdNotFound]; + + testNewMenu = [[NSArray alloc] initWithArray:@[submenuCell, submenuImageCell, textOnlyCell] copyItems:YES]; + + OCMStub([testFileManager uploadArtworks:[OCMArg any] progressHandler:([OCMArg invokeBlockWithArgs:textAndImageCell.icon.name, @1.0, [NSNull null], nil]) completionHandler:([OCMArg invokeBlockWithArgs: @[textAndImageCell.icon.name], [NSNull null], nil])]); + }); + + it(@"should send dynamic deletes first then dynamic adds case with 2 submenu cells", ^{ + testOp = [[SDLMenuReplaceOperation alloc] initWithConnectionManager:testConnectionManager fileManager:testFileManager windowCapability:testWindowCapability menuConfiguration:testMenuConfiguration currentMenu:testCurrentMenu updatedMenu:testNewMenu compatibilityModeEnabled:NO currentMenuUpdatedHandler:testCurrentMenuUpdatedBlock]; + [testOp start]; + + // Delete textOnlyCell + [testConnectionManager respondToLastRequestWithResponse:deleteCommandSuccessResponse]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + expect(testOp.currentMenu).toNot(contain(textOnlyCell)); + + // Add textOnlyCell + [testConnectionManager respondToLastRequestWithResponse:addCommandSuccessResponse]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + + NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; + NSArray *deletes = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; + + NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; + NSArray *adds = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + + NSPredicate *addSubmenuPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddSubMenu class]]; + NSArray *submenu = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addSubmenuPredicate]; + + expect(deletes).to(haveCount(1)); + expect(adds).to(haveCount(1)); + expect(submenu).to(haveCount(0)); + + expect(testOp.isFinished).to(beTrue()); + expect(resultError).to(beNil()); + expect(resultMenuCells).to(haveCount(3)); + }); + }); + + context(@"rearranging cells and their subcells", ^{ + beforeEach(^{ + testCurrentMenu = [[NSArray alloc] initWithArray:@[textOnlyCell, textAndImageCell, submenuCell] copyItems:YES]; + [SDLMenuReplaceUtilities addIdsToMenuCells:testCurrentMenu parentId:ParentIdNotFound]; + + testNewMenu = [[NSArray alloc] initWithArray:@[submenuCellReversed, textAndImageCell, textOnlyCell] copyItems:YES]; + + OCMStub([testFileManager uploadArtworks:[OCMArg any] progressHandler:([OCMArg invokeBlockWithArgs:textAndImageCell.icon.name, @1.0, [NSNull null], nil]) completionHandler:([OCMArg invokeBlockWithArgs: @[textAndImageCell.icon.name], [NSNull null], nil])]); + }); + + it(@"should sent the correct deletions and additions", ^{ + testOp = [[SDLMenuReplaceOperation alloc] initWithConnectionManager:testConnectionManager fileManager:testFileManager windowCapability:testWindowCapability menuConfiguration:testMenuConfiguration currentMenu:testCurrentMenu updatedMenu:testNewMenu compatibilityModeEnabled:NO currentMenuUpdatedHandler:testCurrentMenuUpdatedBlock]; + [testOp start]; + + // Delete textOnlyCell and submenuCell + expect(testConnectionManager.receivedRequests).to(haveCount(2)); + expect(testConnectionManager.receivedRequests[0]).to(beAnInstanceOf(SDLDeleteCommand.class)); + expect(testConnectionManager.receivedRequests[1]).to(beAnInstanceOf(SDLDeleteSubMenu.class)); + + [testConnectionManager respondToRequestWithResponse:deleteCommandSuccessResponse requestNumber:0 error:nil]; + [testConnectionManager respondToRequestWithResponse:deleteSubMenuSuccessResponse requestNumber:1 error:nil]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + expect(testOp.currentMenu).to(haveCount(1)); + + // Main Menu Add Command / Add Submenu + expect(testConnectionManager.receivedRequests).to(haveCount(4)); + + [testConnectionManager respondToRequestWithResponse:addSubMenuSuccessResponse requestNumber:2 error:nil]; + [testConnectionManager respondToRequestWithResponse:addCommandSuccessResponse requestNumber:3 error:nil]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + expect(testOp.currentMenu).to(haveCount(3)); + + NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; + NSArray *deletes = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; + + NSPredicate *deleteSubCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteSubMenu class]]; + NSArray *subDeletes = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteSubCommandPredicate]; + + NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; + NSArray *adds = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + + NSPredicate *addSubmenuPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddSubMenu class]]; + NSArray *submenu = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addSubmenuPredicate]; + + // Submenu add commands sent + expect(deletes).to(haveCount(1)); + expect(subDeletes).to(haveCount(1)); + expect(adds).to(haveCount(51)); + expect(submenu).to(haveCount(1)); + + // Respond to all 50 submenu add commands + for (NSUInteger i = 0; i < 50; i++) { + [testConnectionManager respondToRequestWithResponse:addCommandSuccessResponse requestNumber:(i + 4) error:nil]; + } + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + + expect(testOp.isFinished).to(beTrue()); + expect(resultError).to(beNil()); + expect(resultMenuCells).to(haveCount(3)); + }); + }); + + context(@"removing a cell with subcells", ^{ + beforeEach(^{ + testCurrentMenu = [[NSArray alloc] initWithArray:@[textOnlyCell, textAndImageCell, submenuCell, submenuImageCell] copyItems:YES]; + [SDLMenuReplaceUtilities addIdsToMenuCells:testCurrentMenu parentId:ParentIdNotFound]; + + testNewMenu = [[NSArray alloc] initWithArray:@[textOnlyCell, textAndImageCell, submenuCell] copyItems:YES]; + + OCMStub([testFileManager uploadArtworks:[OCMArg any] progressHandler:([OCMArg invokeBlockWithArgs:textAndImageCell.icon.name, @1.0, [NSNull null], nil]) completionHandler:([OCMArg invokeBlockWithArgs: @[textAndImageCell.icon.name], [NSNull null], nil])]); + }); + + it(@"should send one deletion", ^{ + testOp = [[SDLMenuReplaceOperation alloc] initWithConnectionManager:testConnectionManager fileManager:testFileManager windowCapability:testWindowCapability menuConfiguration:testMenuConfiguration currentMenu:testCurrentMenu updatedMenu:testNewMenu compatibilityModeEnabled:NO currentMenuUpdatedHandler:testCurrentMenuUpdatedBlock]; + [testOp start]; + + // Delete submenuImageCell + [testConnectionManager respondToLastRequestWithResponse:deleteSubMenuSuccessResponse]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + expect(testOp.currentMenu).toNot(contain(submenuImageCell)); + + NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; + NSArray *deletes = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; + + NSPredicate *deleteSubCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteSubMenu class]]; + NSArray *subDeletes = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteSubCommandPredicate]; + + NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; + NSArray *adds = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + + NSPredicate *addSubmenuPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddSubMenu class]]; + NSArray *submenu = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addSubmenuPredicate]; + + expect(deletes).to(haveCount(0)); + expect(subDeletes).to(haveCount(1)); + expect(adds).to(haveCount(0)); + expect(submenu).to(haveCount(0)); + + expect(testOp.isFinished).to(beTrue()); + expect(resultError).to(beNil()); + expect(resultMenuCells).to(haveCount(3)); + }); + }); + + context(@"when cells remain the same", ^{ + __block BOOL secondHandlerCalled = NO; + + beforeEach(^{ + secondHandlerCalled = NO; + + testCurrentMenu = [[NSArray alloc] initWithArray:@[textOnlyCell, textOnlyCell2, textAndImageCell] copyItems:YES]; + [SDLMenuReplaceUtilities addIdsToMenuCells:testCurrentMenu parentId:ParentIdNotFound]; + + textOnlyCell.handler = ^(SDLTriggerSource triggerSource) { + secondHandlerCalled = YES; + }; + testNewMenu = [[NSArray alloc] initWithArray:@[textOnlyCell, textOnlyCell2, textAndImageCell] copyItems:YES]; + }); + + it(@"should not send deletes or adds, but should transfer handlers", ^{ + testOp = [[SDLMenuReplaceOperation alloc] initWithConnectionManager:testConnectionManager fileManager:testFileManager windowCapability:testWindowCapability menuConfiguration:testMenuConfiguration currentMenu:testCurrentMenu updatedMenu:testNewMenu compatibilityModeEnabled:NO currentMenuUpdatedHandler:testCurrentMenuUpdatedBlock]; + [testOp start]; + + NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; + NSArray *deletes = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; + + NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; + NSArray *adds = [[testConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + + expect(deletes).to(haveCount(0)); + expect(adds).to(haveCount(0)); + + expect(testOp.isFinished).to(beTrue()); + expect(resultError).to(beNil()); + expect(resultMenuCells).to(haveCount(3)); + + resultMenuCells[0].handler(SDLTriggerSourceMenu); + expect(secondHandlerCalled).to(beTrue()); + }); + }); + }); +}); + +QuickSpecEnd diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLMenuReplaceUtilitiesSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuReplaceUtilitiesSpec.m new file mode 100644 index 000000000..51f6bca6a --- /dev/null +++ b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuReplaceUtilitiesSpec.m @@ -0,0 +1,532 @@ +#import +#import +#import + +#import "SDLMenuReplaceUtilities.h" + +#import "SDLFileManager.h" +#import "SDLMenuCell.h" +#import "SDLMenuReplaceUtilitiesSpecHelpers.h" +#import "SDLMenuManagerPrivateConstants.h" +#import "SDLWindowCapability.h" +#import "TestConnectionManager.h" + +@interface SDLMenuCell() + +@property (assign, nonatomic) UInt32 parentCellId; +@property (assign, nonatomic) UInt32 cellId; +@property (copy, nonatomic, readwrite, nullable) NSArray *subCells; + +@end + +@interface SDLMenuReplaceUtilities () + +@property (class, assign, nonatomic) UInt32 nextMenuId; + +@end + +QuickSpecBegin(SDLMenuReplaceUtilitiesSpec) + +__block NSMutableArray *testMenuCells = nil; +__block SDLFileManager *mockFileManager = nil; +__block SDLWindowCapability *testWindowCapability = nil; +__block NSArray *allSupportedTextFields = @[ + [[SDLTextField alloc] initWithName:SDLTextFieldNameMenuCommandSecondaryText characterSet:SDLCharacterSetUtf8 width:100 rows:1], + [[SDLTextField alloc] initWithName:SDLTextFieldNameMenuCommandTertiaryText characterSet:SDLCharacterSetUtf8 width:100 rows:1], + [[SDLTextField alloc] initWithName:SDLTextFieldNameMenuSubMenuSecondaryText characterSet:SDLCharacterSetUtf8 width:100 rows:1], + [[SDLTextField alloc] initWithName:SDLTextFieldNameMenuSubMenuTertiaryText characterSet:SDLCharacterSetUtf8 width:100 rows:1] +]; +__block NSArray *allSupportedImageFields = @[ + [[SDLImageField alloc] initWithName:SDLImageFieldNameCommandIcon imageTypeSupported:@[SDLImageTypeDynamic] imageResolution:nil], + [[SDLImageField alloc] initWithName:SDLImageFieldNameMenuCommandSecondaryImage imageTypeSupported:@[SDLImageTypeDynamic] imageResolution:nil], + [[SDLImageField alloc] initWithName:SDLImageFieldNameSubMenuIcon imageTypeSupported:@[SDLImageTypeDynamic] imageResolution:nil], + [[SDLImageField alloc] initWithName:SDLImageFieldNameMenuSubMenuSecondaryImage imageTypeSupported:@[SDLImageTypeDynamic] imageResolution:nil] +]; + +describe(@"adding ids", ^{ + it(@"should properly add ids", ^{ + SDLMenuReplaceUtilities.nextMenuId = 0; + testMenuCells = SDLMenuReplaceUtilitiesSpecHelpers.deepMenu; + + [SDLMenuReplaceUtilities addIdsToMenuCells:testMenuCells parentId:ParentIdNotFound]; + + expect(testMenuCells[0].cellId).to(equal(1)); + expect(testMenuCells[1].cellId).to(equal(6)); + expect(testMenuCells[2].cellId).to(equal(7)); + + NSArray *subCellList1 = testMenuCells[0].subCells; + expect(subCellList1[0].cellId).to(equal(2)); + expect(subCellList1[0].parentCellId).to(equal(1)); + expect(subCellList1[1].cellId).to(equal(5)); + expect(subCellList1[1].parentCellId).to(equal(1)); + + NSArray *subCell1SubCellList1 = subCellList1[0].subCells; + expect(subCell1SubCellList1[0].cellId).to(equal(3)); + expect(subCell1SubCellList1[0].parentCellId).to(equal(2)); + expect(subCell1SubCellList1[1].cellId).to(equal(4)); + expect(subCell1SubCellList1[1].parentCellId).to(equal(2)); + + NSArray *subCellList2 = testMenuCells[2].subCells; + expect(subCellList2[0].cellId).to(equal(8)); + expect(subCellList2[0].parentCellId).to(equal(7)); + expect(subCellList2[1].cellId).to(equal(9)); + expect(subCellList2[1].parentCellId).to(equal(7)); + }); +}); + +describe(@"transferring cell ids", ^{ + it(@"should properly transfer ids and set parent ids", ^{ + testMenuCells = [[NSMutableArray alloc] initWithArray:SDLMenuReplaceUtilitiesSpecHelpers.deepMenu copyItems:YES]; + [SDLMenuReplaceUtilities addIdsToMenuCells:testMenuCells parentId:ParentIdNotFound]; + + NSArray *toCells = [[NSArray alloc] initWithArray:SDLMenuReplaceUtilitiesSpecHelpers.deepMenu copyItems:YES]; + [SDLMenuReplaceUtilities transferCellIDsFromCells:testMenuCells toCells:toCells]; + + // Top-level cells should have same cell ids + for (NSUInteger i = 0; i < testMenuCells.count; i++) { + expect(toCells[i].cellId).to(equal(testMenuCells[i].cellId)); + } + + // Sub-cells should _not_ have the same cell ids + for (NSUInteger i = 0; i < testMenuCells[0].subCells.count; i++) { + expect(toCells[0].subCells[i].cellId).toNot(equal(testMenuCells[0].subCells[i].cellId)); + } + + // Sub-cells should have proper parent ids + for (NSUInteger i = 0; i < testMenuCells[0].subCells.count; i++) { + expect(toCells[0].subCells[i].parentCellId).to(equal(toCells[0].cellId)); + } + }); +}); + +describe(@"transferring cell handlers", ^{ + __block BOOL cell1HandlerTriggered = NO; + __block BOOL cell2HandlerTriggered = NO; + beforeEach(^{ + cell1HandlerTriggered = NO; + cell2HandlerTriggered = NO; + }); + + it(@"should properly transfer cell handlers", ^{ + SDLMenuCell *cell1 = [[SDLMenuCell alloc] initWithTitle:@"Cell1" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) { + cell1HandlerTriggered = YES; + }]; + SDLMenuCell *cell2 = [[SDLMenuCell alloc] initWithTitle:@"Cell1" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) { + cell2HandlerTriggered = YES; + }]; + + [SDLMenuReplaceUtilities transferCellHandlersFromCells:@[cell1] toCells:@[cell2]]; + cell2.handler(SDLTriggerSourceMenu); + + expect(cell1HandlerTriggered).to(beTrue()); + expect(cell2HandlerTriggered).to(beFalse()); + }); +}); + +describe(@"finding all artworks from cells", ^{ + beforeEach(^{ + mockFileManager = OCMClassMock([SDLFileManager class]); + testWindowCapability = [[SDLWindowCapability alloc] init]; + }); + + context(@"when all the files need to be uploaded", ^{ + beforeEach(^{ + OCMStub([mockFileManager fileNeedsUpload:[OCMArg any]]).andReturn(YES); + }); + + context(@"when the window capability doesn't support the primary image", ^{ + beforeEach(^{ + testWindowCapability.textFields = allSupportedTextFields; + }); + + it(@"should return an empty list of artworks to upload", ^{ + NSArray *artworksToUpload = [SDLMenuReplaceUtilities findAllArtworksToBeUploadedFromCells:SDLMenuReplaceUtilitiesSpecHelpers.topLevelOnlyMenu fileManager:mockFileManager windowCapability:testWindowCapability]; + + expect(artworksToUpload).to(beEmpty()); + }); + }); + + context(@"when the window capability supports primary but not secondary image", ^{ + beforeEach(^{ + testWindowCapability.textFields = allSupportedTextFields; + testWindowCapability.imageFields = @[allSupportedImageFields[0], allSupportedImageFields[2]]; + }); + + it(@"should only return primary images to upload", ^{ + NSArray *artworksToUpload = [SDLMenuReplaceUtilities findAllArtworksToBeUploadedFromCells:SDLMenuReplaceUtilitiesSpecHelpers.topLevelOnlyMenu fileManager:mockFileManager windowCapability:testWindowCapability]; + + expect(artworksToUpload).to(haveCount(2)); + }); + }); + + context(@"when the window capability supports both images", ^{ + beforeEach(^{ + testWindowCapability.textFields = allSupportedTextFields; + testWindowCapability.imageFields = allSupportedImageFields; + }); + + context(@"with a shallow menu", ^{ + it(@"should only return all images to upload", ^{ + NSArray *artworksToUpload = [SDLMenuReplaceUtilities findAllArtworksToBeUploadedFromCells:SDLMenuReplaceUtilitiesSpecHelpers.topLevelOnlyMenu fileManager:mockFileManager windowCapability:testWindowCapability]; + + expect(artworksToUpload).to(haveCount(2)); + }); + }); + + context(@"with a deep menu", ^{ + it(@"should only return all images to upload", ^{ + NSArray *artworksToUpload = [SDLMenuReplaceUtilities findAllArtworksToBeUploadedFromCells:SDLMenuReplaceUtilitiesSpecHelpers.deepMenu fileManager:mockFileManager windowCapability:testWindowCapability]; + + expect(artworksToUpload).to(haveCount(4)); + }); + }); + }); + }); + + context(@"when no files need to be uploaded", ^{ + beforeEach(^{ + OCMStub([mockFileManager fileNeedsUpload:[OCMArg any]]).andReturn(NO); + }); + + context(@"when the window capability supports both images", ^{ + beforeEach(^{ + testWindowCapability.textFields = allSupportedTextFields; + testWindowCapability.imageFields = allSupportedImageFields; + }); + + it(@"should not return any images to upload", ^{ + NSArray *artworksToUpload = [SDLMenuReplaceUtilities findAllArtworksToBeUploadedFromCells:SDLMenuReplaceUtilitiesSpecHelpers.topLevelOnlyMenu fileManager:mockFileManager windowCapability:testWindowCapability]; + + expect(artworksToUpload).to(beEmpty()); + }); + }); + }); +}); + +describe(@"retrieving a commandId", ^{ + context(@"with an AddCommand", ^{ + it(@"should return the command id", ^{ + SDLAddCommand *rpc = [[SDLAddCommand alloc] init]; + rpc.cmdID = @12345; + expect([SDLMenuReplaceUtilities commandIdForRPCRequest:rpc]).to(equal(12345)); + }); + }); + + context(@"with an AddSubMenu", ^{ + it(@"should return the command id", ^{ + SDLAddSubMenu *rpc = [[SDLAddSubMenu alloc] init]; + rpc.menuID = @12345; + expect([SDLMenuReplaceUtilities commandIdForRPCRequest:rpc]).to(equal(12345)); + }); + }); + + context(@"with a DeleteCommand", ^{ + it(@"should return the command id", ^{ + SDLDeleteCommand *rpc = [[SDLDeleteCommand alloc] init]; + rpc.cmdID = @12345; + expect([SDLMenuReplaceUtilities commandIdForRPCRequest:rpc]).to(equal(12345)); + }); + }); + + context(@"with a DeleteSubMenu", ^{ + it(@"should return the command id", ^{ + SDLDeleteSubMenu *rpc = [[SDLDeleteSubMenu alloc] init]; + rpc.menuID = @12345; + expect([SDLMenuReplaceUtilities commandIdForRPCRequest:rpc]).to(equal(12345)); + }); + }); + + context(@"with an Alert", ^{ + it(@"should return 0", ^{ + SDLAlert *rpc = [[SDLAlert alloc] init]; + expect([SDLMenuReplaceUtilities commandIdForRPCRequest:rpc]).to(equal(0)); + }); + }); +}); + +describe(@"retrieving a position", ^{ + context(@"with an AddCommand", ^{ + it(@"should return the position", ^{ + SDLAddCommand *rpc = [[SDLAddCommand alloc] init]; + rpc.menuParams = [[SDLMenuParams alloc] init]; + rpc.menuParams.position = @123; + expect(@([SDLMenuReplaceUtilities positionForRPCRequest:rpc])).to(equal(@123)); + }); + }); + + context(@"with an AddSubMenu", ^{ + it(@"should return the command id", ^{ + SDLAddSubMenu *rpc = [[SDLAddSubMenu alloc] init]; + rpc.position = @123; + expect(@([SDLMenuReplaceUtilities positionForRPCRequest:rpc])).to(equal(@123)); + }); + }); +}); + +describe(@"generating RPCs", ^{ + __block SDLMenuLayout testMenuLayout = SDLMenuLayoutList; + + beforeEach(^{ + mockFileManager = OCMClassMock([SDLFileManager class]); + testWindowCapability = [[SDLWindowCapability alloc] init]; + }); + + context(@"delete commands", ^{ + context(@"shallow menu", ^{ + beforeEach(^{ + testMenuCells = SDLMenuReplaceUtilitiesSpecHelpers.topLevelOnlyMenu; + }); + + it(@"should generate the correct RPCs", ^{ + NSArray *requests = [SDLMenuReplaceUtilities deleteCommandsForCells:testMenuCells]; + expect(requests).to(haveCount(3)); + expect(requests[0]).to(beAnInstanceOf(SDLDeleteCommand.class)); + expect(requests[1]).to(beAnInstanceOf(SDLDeleteCommand.class)); + expect(requests[2]).to(beAnInstanceOf(SDLDeleteCommand.class)); + }); + }); + + context(@"deep menu", ^{ + beforeEach(^{ + testMenuCells = SDLMenuReplaceUtilitiesSpecHelpers.deepMenu; + }); + + it(@"should generate the correct RPCs", ^{ + NSArray *requests = [SDLMenuReplaceUtilities deleteCommandsForCells:testMenuCells]; + expect(requests).to(haveCount(3)); + expect(requests[0]).to(beAnInstanceOf(SDLDeleteSubMenu.class)); + expect(requests[1]).to(beAnInstanceOf(SDLDeleteCommand.class)); + expect(requests[2]).to(beAnInstanceOf(SDLDeleteSubMenu.class)); + }); + }); + }); + + context(@"main menu commands", ^{ + context(@"shallow menu", ^{ + beforeEach(^{ + testMenuCells = SDLMenuReplaceUtilitiesSpecHelpers.topLevelOnlyMenu; + }); + + it(@"should generate the correct RPCs", ^{ + NSArray *requests = [SDLMenuReplaceUtilities mainMenuCommandsForCells:testMenuCells fileManager:mockFileManager usingPositionsFromFullMenu:testMenuCells windowCapability:testWindowCapability defaultSubmenuLayout:testMenuLayout]; + expect(requests).to(haveCount(3)); + expect(requests[0]).to(beAnInstanceOf(SDLAddCommand.class)); + expect(requests[1]).to(beAnInstanceOf(SDLAddCommand.class)); + expect(requests[2]).to(beAnInstanceOf(SDLAddCommand.class)); + }); + }); + + context(@"deep menu", ^{ + beforeEach(^{ + testMenuCells = SDLMenuReplaceUtilitiesSpecHelpers.deepMenu; + }); + + it(@"should generate the correct RPCs", ^{ + NSArray *requests = [SDLMenuReplaceUtilities mainMenuCommandsForCells:testMenuCells fileManager:mockFileManager usingPositionsFromFullMenu:testMenuCells windowCapability:testWindowCapability defaultSubmenuLayout:testMenuLayout]; + expect(requests).to(haveCount(3)); + expect(requests[0]).to(beAnInstanceOf(SDLAddSubMenu.class)); + expect(requests[1]).to(beAnInstanceOf(SDLAddCommand.class)); + expect(requests[2]).to(beAnInstanceOf(SDLAddSubMenu.class)); + }); + }); + }); + + context(@"sub menu commands", ^{ + context(@"shallow menu", ^{ + beforeEach(^{ + testMenuCells = SDLMenuReplaceUtilitiesSpecHelpers.topLevelOnlyMenu; + }); + }); + + context(@"deep menu", ^{ + beforeEach(^{ + testMenuCells = SDLMenuReplaceUtilitiesSpecHelpers.deepMenu; + }); + }); + }); +}); + +// updating menu cells +describe(@"updating menu cell lists", ^{ + __block UInt32 testCommandId = 0; + + describe(@"removing commands", ^{ + context(@"from a shallow list", ^{ + beforeEach(^{ + testMenuCells = SDLMenuReplaceUtilitiesSpecHelpers.topLevelOnlyMenu; + [SDLMenuReplaceUtilities addIdsToMenuCells:testMenuCells parentId:ParentIdNotFound]; + }); + + context(@"when the cell is in the menu", ^{ + beforeEach(^{ + testCommandId = testMenuCells[1].cellId; + }); + + it(@"should return the menu without the cell and return YES", ^{ + NSMutableArray *testMutableMenuCells = [testMenuCells mutableCopy]; + BOOL foundItem = [SDLMenuReplaceUtilities removeCellFromList:testMutableMenuCells withCellId:testCommandId]; + + expect(foundItem).to(beTrue()); + expect(testMutableMenuCells).to(haveCount(2)); + expect(testMutableMenuCells[0]).to(equal(testMenuCells[0])); + expect(testMutableMenuCells[1]).to(equal(testMenuCells[2])); + }); + }); + + context(@"when the cell is not in the menu", ^{ + beforeEach(^{ + testCommandId = 100; + }); + + it(@"should return the menu with all cells and return NO", ^{ + NSMutableArray *testMutableMenuCells = [testMenuCells mutableCopy]; + BOOL foundItem = [SDLMenuReplaceUtilities removeCellFromList:testMutableMenuCells withCellId:testCommandId]; + + expect(foundItem).to(beFalse()); + expect(testMutableMenuCells).to(haveCount(3)); + }); + }); + }); + + context(@"from a deep list", ^{ + beforeEach(^{ + testMenuCells = SDLMenuReplaceUtilitiesSpecHelpers.deepMenu; + [SDLMenuReplaceUtilities addIdsToMenuCells:testMenuCells parentId:ParentIdNotFound]; + }); + + context(@"when the cell is in the top menu", ^{ + beforeEach(^{ + testCommandId = testMenuCells[1].cellId; + }); + + it(@"should return the menu without the cell and return YES", ^{ + NSMutableArray *testMutableMenuCells = [testMenuCells mutableCopy]; + BOOL foundItem = [SDLMenuReplaceUtilities removeCellFromList:testMutableMenuCells withCellId:testCommandId]; + + expect(foundItem).to(beTrue()); + expect(testMutableMenuCells).to(haveCount(2)); + expect(testMutableMenuCells[0]).to(equal(testMenuCells[0])); + expect(testMutableMenuCells[1]).to(equal(testMenuCells[2])); + }); + }); + + context(@"when the cell is in the submenu", ^{ + beforeEach(^{ + testCommandId = testMenuCells[0].subCells[0].cellId; + }); + + it(@"should return the menu without the cell and return YES", ^{ + NSMutableArray *testMutableMenuCells = [testMenuCells mutableCopy]; + BOOL foundItem = [SDLMenuReplaceUtilities removeCellFromList:testMutableMenuCells withCellId:testCommandId]; + + expect(foundItem).to(beTrue()); + expect(testMutableMenuCells).to(haveCount(3)); + expect(testMutableMenuCells[0].subCells).to(haveCount(1)); + }); + }); + + context(@"when the cell is not in the menu", ^{ + beforeEach(^{ + testCommandId = 100; + }); + + it(@"should return the menu with all cells and return NO", ^{ + NSMutableArray *testMutableMenuCells = [testMenuCells mutableCopy]; + BOOL foundItem = [SDLMenuReplaceUtilities removeCellFromList:testMutableMenuCells withCellId:testCommandId]; + + expect(foundItem).to(beFalse()); + expect(testMutableMenuCells).to(haveCount(3)); + expect(testMutableMenuCells[0].subCells).to(haveCount(2)); + expect(testMutableMenuCells[2].subCells).to(haveCount(2)); + }); + }); + }); + }); + + describe(@"add commands to the main list", ^{ + __block NSMutableArray *newCellList = nil; + + context(@"from a shallow list", ^{ + beforeEach(^{ + testMenuCells = SDLMenuReplaceUtilitiesSpecHelpers.topLevelOnlyMenu; + [SDLMenuReplaceUtilities addIdsToMenuCells:testMenuCells parentId:ParentIdNotFound]; + + SDLMenuCell *newCell = [[SDLMenuCell alloc] initWithTitle:@"New Cell" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; + newCell.cellId = 99; + newCellList = [@[newCell] mutableCopy]; + }); + + describe(@"if the cell is not in the cell list", ^{ + beforeEach(^{ + newCellList = [[NSMutableArray alloc] init]; + }); + + it(@"should return NO", ^{ + BOOL didAddCell = [SDLMenuReplaceUtilities addCellWithCellId:99 position:0 fromNewMenuList:newCellList toMainMenuList:testMenuCells]; + + expect(didAddCell).to(beFalse()); + }); + }); + + context(@"at the beginning", ^{ + it(@"should return YES and the cell should be included", ^{ + BOOL didAddCell = [SDLMenuReplaceUtilities addCellWithCellId:newCellList[0].cellId position:0 fromNewMenuList:newCellList toMainMenuList:testMenuCells]; + + expect(didAddCell).to(beTrue()); + expect(testMenuCells).to(haveCount(4)); + expect(testMenuCells[0]).to(equal(newCellList[0])); + }); + }); + + context(@"in the middle", ^{ + it(@"should return YES and the cell should be included", ^{ + BOOL didAddCell = [SDLMenuReplaceUtilities addCellWithCellId:newCellList[0].cellId position:1 fromNewMenuList:newCellList toMainMenuList:testMenuCells]; + + expect(didAddCell).to(beTrue()); + expect(testMenuCells).to(haveCount(4)); + expect(testMenuCells[1]).to(equal(newCellList[0])); + }); + }); + + context(@"at the end", ^{ + it(@"should return YES and the cell should be included", ^{ + BOOL didAddCell = [SDLMenuReplaceUtilities addCellWithCellId:newCellList[0].cellId position:3 fromNewMenuList:newCellList toMainMenuList:testMenuCells]; + + expect(didAddCell).to(beTrue()); + expect(testMenuCells).to(haveCount(4)); + expect(testMenuCells[3]).to(equal(newCellList[0])); + }); + }); + }); + + context(@"from a deep list", ^{ + __block SDLMenuCell *subCell = nil; + __block NSMutableArray *newMenu = nil; + + beforeEach(^{ + testMenuCells = SDLMenuReplaceUtilitiesSpecHelpers.deepMenu.copy; + [SDLMenuReplaceUtilities addIdsToMenuCells:testMenuCells parentId:ParentIdNotFound]; + + newMenu = [[NSMutableArray alloc] initWithArray:testMenuCells copyItems:YES]; + NSMutableArray *subMenuToUpdate = newMenu[0].subCells.mutableCopy; + + subCell = [[SDLMenuCell alloc] initWithTitle:@"New SubCell" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; + subCell.cellId = 98; + subCell.parentCellId = newMenu[0].cellId; + [subMenuToUpdate insertObject:subCell atIndex:0]; + newMenu[0].subCells = subMenuToUpdate.copy; + }); + + it(@"should properly add the subcell to the list", ^{ + BOOL didAddCell = [SDLMenuReplaceUtilities addCellWithCellId:newMenu[0].subCells[0].cellId position:0 fromNewMenuList:newMenu toMainMenuList:testMenuCells]; + + expect(didAddCell).to(beTrue()); + expect(testMenuCells).to(haveCount(3)); + expect(testMenuCells[0].subCells).to(haveCount(3)); + expect(testMenuCells[0].subCells[0]).to(equal(subCell)); + }); + }); + }); +}); + +QuickSpecEnd diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLMenuReplaceUtilitiesSpecHelpers.h b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuReplaceUtilitiesSpecHelpers.h new file mode 100644 index 000000000..7106cb567 --- /dev/null +++ b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuReplaceUtilitiesSpecHelpers.h @@ -0,0 +1,22 @@ +// +// SDLMenuReplaceUtilitiesSpecHelpers.h +// SmartDeviceLinkTests +// +// Created by Joel Fischer on 1/29/21. +// Copyright © 2021 smartdevicelink. All rights reserved. +// + +#import + +@class SDLMenuCell; + +NS_ASSUME_NONNULL_BEGIN + +@interface SDLMenuReplaceUtilitiesSpecHelpers : NSObject + +@property (class, nonatomic, readonly) NSMutableArray *topLevelOnlyMenu; +@property (class, nonatomic, readonly) NSMutableArray *deepMenu; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLMenuReplaceUtilitiesSpecHelpers.m b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuReplaceUtilitiesSpecHelpers.m new file mode 100644 index 000000000..c6069d315 --- /dev/null +++ b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuReplaceUtilitiesSpecHelpers.m @@ -0,0 +1,70 @@ +// +// SDLMenuReplaceUtilitiesSpecHelpers.m +// SmartDeviceLinkTests +// +// Created by Joel Fischer on 1/29/21. +// Copyright © 2021 smartdevicelink. All rights reserved. +// + +#import "SDLMenuReplaceUtilitiesSpecHelpers.h" + +#import "SDLArtwork.h" +#import "SDLMenuCell.h" + +@interface SDLMenuCell() + +@property (assign, nonatomic) UInt32 parentCellId; +@property (assign, nonatomic) UInt32 cellId; + +@end + +@implementation SDLMenuReplaceUtilitiesSpecHelpers + ++ (NSMutableArray *)topLevelOnlyMenu { + NSData *cellArtData = [@"testart" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *cellArtData2 = [@"testart2" dataUsingEncoding:NSUTF8StringEncoding]; + SDLArtwork *artwork1 = [[SDLArtwork alloc] initWithData:cellArtData name:@"Test Art 1" fileExtension:@"png" persistent:NO]; + SDLArtwork *artwork2 = [[SDLArtwork alloc] initWithData:cellArtData2 name:@"Test Art 2" fileExtension:@"png" persistent:NO]; + + SDLMenuCell *cell1 = [[SDLMenuCell alloc] initWithTitle:@"Item 1" secondaryText:nil tertiaryText:nil icon:artwork1 secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; + cell1.cellId = 1; + + SDLMenuCell *cell2 = [[SDLMenuCell alloc] initWithTitle:@"Item 2" secondaryText:nil tertiaryText:nil icon:artwork1 secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; + cell2.cellId = 2; + + SDLMenuCell *cell3 = [[SDLMenuCell alloc] initWithTitle:@"Item 3" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:artwork2 voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; + cell3.cellId = 3; + + return [@[cell1, cell2, cell3] mutableCopy]; +} + ++ (NSMutableArray *)deepMenu { + NSData *cellArtData = [@"testart" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *cellArtData2 = [@"testart2" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *cellArtData3 = [@"testart3" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *cellArtData4 = [@"testart4" dataUsingEncoding:NSUTF8StringEncoding]; + SDLArtwork *artwork1 = [[SDLArtwork alloc] initWithData:cellArtData name:@"Test Art 1" fileExtension:@"png" persistent:NO]; + SDLArtwork *artwork2 = [[SDLArtwork alloc] initWithData:cellArtData2 name:@"Test Art 2" fileExtension:@"png" persistent:NO]; + SDLArtwork *artwork3 = [[SDLArtwork alloc] initWithData:cellArtData3 name:@"Test Art 3" fileExtension:@"png" persistent:NO]; + SDLArtwork *artwork4 = [[SDLArtwork alloc] initWithData:cellArtData4 name:@"Test Art 4" fileExtension:@"png" persistent:NO]; + + SDLMenuCell *subList1SubList1Cell1 = [[SDLMenuCell alloc] initWithTitle:@"Item 1" secondaryText:@"SubItem 1" tertiaryText:@"Sub-SubItem 1" icon:nil secondaryArtwork:artwork3 voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; + SDLMenuCell *subList1SubList1Cell2 = [[SDLMenuCell alloc] initWithTitle:@"Item 1" secondaryText:@"SubItem 1" tertiaryText:@"Sub-SubItem 2" icon:artwork1 secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; + NSArray *subList1SubList1 = @[subList1SubList1Cell1, subList1SubList1Cell2]; + + SDLMenuCell *subList1Cell1 = [[SDLMenuCell alloc] initWithTitle:@"Item 1" secondaryText:@"SubItem 1" tertiaryText:nil icon:artwork4 secondaryArtwork:nil submenuLayout:nil subCells:subList1SubList1]; + SDLMenuCell *subList1Cell2 = [[SDLMenuCell alloc] initWithTitle:@"Item 1" secondaryText:@"SubItem 2" tertiaryText:nil icon:artwork2 secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; + NSArray *subList1 = @[subList1Cell1, subList1Cell2]; + + SDLMenuCell *subList2Cell1 = [[SDLMenuCell alloc] initWithTitle:@"Item 3" secondaryText:@"SubItem 1" tertiaryText:nil icon:artwork1 secondaryArtwork:artwork4 voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; + SDLMenuCell *subList2Cell2 = [[SDLMenuCell alloc] initWithTitle:@"Item 3" secondaryText:@"SubItem 2" tertiaryText:nil icon:artwork1 secondaryArtwork:artwork2 voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; + NSArray *subList2 = @[subList2Cell1, subList2Cell2]; + + SDLMenuCell *topListCell1 = [[SDLMenuCell alloc] initWithTitle:@"Item 1" secondaryText:nil tertiaryText:nil icon:artwork1 secondaryArtwork:nil submenuLayout:nil subCells:subList1]; + SDLMenuCell *topListCell2 = [[SDLMenuCell alloc] initWithTitle:@"Item 2" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; + SDLMenuCell *topListCell3 = [[SDLMenuCell alloc] initWithTitle:@"Item 3" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil submenuLayout:nil subCells:subList2]; + + return @[topListCell1, topListCell2, topListCell3].mutableCopy; +} + +@end diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLMenuShowOperationSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuShowOperationSpec.m new file mode 100644 index 000000000..d79f2b90f --- /dev/null +++ b/SmartDeviceLinkTests/DevAPISpecs/SDLMenuShowOperationSpec.m @@ -0,0 +1,167 @@ +// +// SDLMenuShowOperationSpec.m +// SmartDeviceLinkTests +// +// Created by Joel Fischer on 2/16/21. +// Copyright © 2021 smartdevicelink. All rights reserved. +// + +#import +#import +#import + +#import +#import "SDLMenuShowOperation.h" +#import "TestConnectionManager.h" + +@interface SDLMenuShowOperation () + +@property (strong, nonatomic, nullable) SDLMenuCell *submenuCell; + +@end + +QuickSpecBegin(SDLMenuShowOperationSpec) + +describe(@"the show menu operation", ^{ + __block SDLMenuShowOperation *testOp = nil; + __block TestConnectionManager *testConnectionManager = nil; + __block NSError *resultError = nil; + __block BOOL callbackCalled = NO; + + beforeEach(^{ + testConnectionManager = [[TestConnectionManager alloc] init]; + testOp = [[SDLMenuShowOperation alloc] initWithConnectionManager:testConnectionManager toMenuCell:nil completionHandler:^(NSError * _Nullable error) { + resultError = error; + callbackCalled = YES; + }]; + resultError = nil; + callbackCalled = NO; + }); + + // opening to the main menu + context(@"opening to the main menu", ^{ + beforeEach(^{ + [testOp start]; + }); + + it(@"should send the RPC request", ^{ + expect(testConnectionManager.receivedRequests).to(haveCount(1)); + }); + + // when the response is not SUCCESS or WARNINGS + context(@"when the response is not SUCCESS or WARNINGS", ^{ + beforeEach(^{ + SDLShowAppMenuResponse *response = [[SDLShowAppMenuResponse alloc] init]; + response.success = @NO; + response.resultCode = SDLResultRejected; + + [testConnectionManager respondToLastRequestWithResponse:response]; + }); + + it(@"should set the error and finish", ^{ + expect(testOp.isFinished).to(beTrue()); + expect(resultError).toNot(beNil()); + expect(callbackCalled).to(beTrue()); + }); + }); + + // when the response is SUCCESS + context(@"when the response is SUCCESS", ^{ + beforeEach(^{ + SDLShowAppMenuResponse *response = [[SDLShowAppMenuResponse alloc] init]; + response.success = @YES; + response.resultCode = SDLResultSuccess; + + [testConnectionManager respondToLastRequestWithResponse:response]; + }); + + it(@"should not set the error and finish", ^{ + expect(testOp.isFinished).to(beTrue()); + expect(resultError).to(beNil()); + expect(callbackCalled).to(beTrue()); + }); + }); + + // when the response is WARNINGS + context(@"when the response is WARNINGS", ^{ + beforeEach(^{ + SDLShowAppMenuResponse *response = [[SDLShowAppMenuResponse alloc] init]; + response.success = @YES; + response.resultCode = SDLResultWarnings; + + [testConnectionManager respondToLastRequestWithResponse:response]; + }); + + it(@"should not set the error and finish", ^{ + expect(testOp.isFinished).to(beTrue()); + expect(resultError).to(beNil()); + expect(callbackCalled).to(beTrue()); + }); + }); + }); + + // opening to an inner menu + context(@"opening to an inner menu", ^{ + __block SDLMenuCell *openToCell = nil; + __block SDLMenuCell *subcell = nil; + beforeEach(^{ + subcell = [[SDLMenuCell alloc] initWithTitle:@"Subcell" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) { }]; + openToCell = [[SDLMenuCell alloc] initWithTitle:@"Test submenu" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil submenuLayout:nil subCells:@[subcell]]; + testOp.submenuCell = openToCell; + [testOp start]; + }); + + // when the response is not SUCCESS or WARNINGS + context(@"when the response is not SUCCESS or WARNINGS", ^{ + beforeEach(^{ + SDLShowAppMenuResponse *response = [[SDLShowAppMenuResponse alloc] init]; + response.success = @NO; + response.resultCode = SDLResultRejected; + + [testConnectionManager respondToLastRequestWithResponse:response]; + }); + + it(@"should set the error and finish", ^{ + expect(resultError).toNot(beNil()); + expect(callbackCalled).to(beTrue()); + expect(testOp.isFinished).to(beTrue()); + }); + }); + + // when the response is SUCCESS + context(@"when the response is SUCCESS", ^{ + beforeEach(^{ + SDLShowAppMenuResponse *response = [[SDLShowAppMenuResponse alloc] init]; + response.success = @YES; + response.resultCode = SDLResultSuccess; + + [testConnectionManager respondToLastRequestWithResponse:response]; + }); + + it(@"should not set the error and finish", ^{ + expect(resultError).to(beNil()); + expect(callbackCalled).to(beTrue()); + expect(testOp.isFinished).to(beTrue()); + }); + }); + + // when the response is WARNINGS + context(@"when the response is WARNINGS", ^{ + beforeEach(^{ + SDLShowAppMenuResponse *response = [[SDLShowAppMenuResponse alloc] init]; + response.success = @YES; + response.resultCode = SDLResultWarnings; + + [testConnectionManager respondToLastRequestWithResponse:response]; + }); + + it(@"should not set the error and finish", ^{ + expect(resultError).to(beNil()); + expect(callbackCalled).to(beTrue()); + expect(testOp.isFinished).to(beTrue()); + }); + }); + }); +}); + +QuickSpecEnd diff --git a/SmartDeviceLinkTests/SDLMenuUpdateAlgorithmSpec.m b/SmartDeviceLinkTests/SDLMenuUpdateAlgorithmSpec.m index 63eb97f95..92f3e9abb 100644 --- a/SmartDeviceLinkTests/SDLMenuUpdateAlgorithmSpec.m +++ b/SmartDeviceLinkTests/SDLMenuUpdateAlgorithmSpec.m @@ -22,7 +22,7 @@ typedef NS_ENUM(NSUInteger, MenuCellState) { MenuCellStateKeep }; -describe(@"menuUpdateAlgorithm", ^{ +describe(@"The menu update algorithm", ^{ __block SDLDynamicMenuUpdateRunScore *runScore = nil; __block SDLMenuCell *oldCell1 = nil; @@ -39,7 +39,7 @@ typedef NS_ENUM(NSUInteger, MenuCellState) { __block SDLMenuCell *newCell5 = nil; __block SDLMenuCell *newCell6 = nil; - // 0 = Delete 1 = Add 2 = Keep + // 0 = Delete, 1 = Add, 2 = Keep describe(@"compare old and new menu cells", ^{ beforeEach(^{ oldCell1 = [[SDLMenuCell alloc] initWithTitle:@"Cell 1" secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) {}]; @@ -58,11 +58,10 @@ typedef NS_ENUM(NSUInteger, MenuCellState) { }); it(@"should have a new menu status of 22221 and an old menu status of 2222 on best run", ^{ - NSArray *oldMenuCells = @[oldCell1, oldCell2, oldCell3, oldCell4]; NSArray *updatedMenuCells = @[newCell1, newCell2, newCell3, newCell4, newCell5]; - runScore = [SDLDynamicMenuUpdateAlgorithm compareOldMenuCells:oldMenuCells updatedMenuCells:updatedMenuCells]; + runScore = [SDLDynamicMenuUpdateAlgorithm dynamicRunScoreOldMenuCells:oldMenuCells updatedMenuCells:updatedMenuCells]; expect(runScore.updatedStatus.count).to(equal(5)); expect(runScore.oldStatus.count).to(equal(4)); @@ -85,7 +84,7 @@ typedef NS_ENUM(NSUInteger, MenuCellState) { NSArray *oldMenuCells = @[oldCell1, oldCell2, oldCell3, oldCell4]; NSArray *updatedMenuCells = @[newCell1, newCell2, newCell3]; - runScore = [SDLDynamicMenuUpdateAlgorithm compareOldMenuCells:oldMenuCells updatedMenuCells:updatedMenuCells]; + runScore = [SDLDynamicMenuUpdateAlgorithm dynamicRunScoreOldMenuCells:oldMenuCells updatedMenuCells:updatedMenuCells]; expect(runScore.updatedStatus.count).to(equal(3)); expect(runScore.oldStatus.count).to(equal(4)); @@ -105,7 +104,7 @@ typedef NS_ENUM(NSUInteger, MenuCellState) { NSArray *oldMenuCells = @[oldCell1, oldCell2, oldCell3]; NSArray *updatedMenuCells = @[newCell4, newCell5, newCell6]; - runScore = [SDLDynamicMenuUpdateAlgorithm compareOldMenuCells:oldMenuCells updatedMenuCells:updatedMenuCells]; + runScore = [SDLDynamicMenuUpdateAlgorithm dynamicRunScoreOldMenuCells:oldMenuCells updatedMenuCells:updatedMenuCells]; expect(runScore.updatedStatus.count).to(equal(3)); expect(runScore.oldStatus.count).to(equal(3)); @@ -124,7 +123,7 @@ typedef NS_ENUM(NSUInteger, MenuCellState) { NSArray *oldMenuCells = @[oldCell1, oldCell2, oldCell3, oldCell4]; NSArray *updatedMenuCells = @[oldCell2, oldCell1, oldCell4, oldCell3 ]; - runScore = [SDLDynamicMenuUpdateAlgorithm compareOldMenuCells:oldMenuCells updatedMenuCells:updatedMenuCells]; + runScore = [SDLDynamicMenuUpdateAlgorithm dynamicRunScoreOldMenuCells:oldMenuCells updatedMenuCells:updatedMenuCells]; expect(runScore.updatedStatus.count).to(equal(4)); expect(runScore.oldStatus.count).to(equal(4)); @@ -145,7 +144,7 @@ typedef NS_ENUM(NSUInteger, MenuCellState) { NSArray *oldMenuCells = @[oldCell1, oldCell2, oldCell3, oldCell4]; NSArray *updatedMenuCells = @[]; - runScore = [SDLDynamicMenuUpdateAlgorithm compareOldMenuCells:oldMenuCells updatedMenuCells:updatedMenuCells]; + runScore = [SDLDynamicMenuUpdateAlgorithm dynamicRunScoreOldMenuCells:oldMenuCells updatedMenuCells:updatedMenuCells]; expect(runScore.updatedStatus.count).to(equal(0)); expect(runScore.oldStatus.count).to(equal(4)); @@ -161,7 +160,7 @@ typedef NS_ENUM(NSUInteger, MenuCellState) { NSArray *oldMenuCells = @[]; NSArray *updatedMenuCells = @[oldCell1, oldCell2, oldCell3, oldCell4]; - runScore = [SDLDynamicMenuUpdateAlgorithm compareOldMenuCells:oldMenuCells updatedMenuCells:updatedMenuCells]; + runScore = [SDLDynamicMenuUpdateAlgorithm dynamicRunScoreOldMenuCells:oldMenuCells updatedMenuCells:updatedMenuCells]; expect(runScore.updatedStatus.count).to(equal(4)); expect(runScore.oldStatus.count).to(equal(0)); @@ -178,9 +177,9 @@ typedef NS_ENUM(NSUInteger, MenuCellState) { NSArray *oldMenuCells = @[]; NSArray *updatedMenuCells = @[]; - runScore = [SDLDynamicMenuUpdateAlgorithm compareOldMenuCells:oldMenuCells updatedMenuCells:updatedMenuCells]; + runScore = [SDLDynamicMenuUpdateAlgorithm dynamicRunScoreOldMenuCells:oldMenuCells updatedMenuCells:updatedMenuCells]; - expect(runScore).to(beNil()); + expect(runScore.isEmpty).to(beTrue()); }); }); }); diff --git a/SmartDeviceLinkTests/TestUtilities/TestConnectionManager.m b/SmartDeviceLinkTests/TestUtilities/TestConnectionManager.m index 525ad3052..2312e6084 100644 --- a/SmartDeviceLinkTests/TestUtilities/TestConnectionManager.m +++ b/SmartDeviceLinkTests/TestUtilities/TestConnectionManager.m @@ -55,6 +55,10 @@ - (void)sendConnectionManagerRPC:(__kindof SDLRPCMessage *)rpc { } - (void)sendRequests:(nonnull NSArray *)requests progressHandler:(nullable SDLMultipleAsyncRequestProgressHandler)progressHandler completionHandler:(nullable SDLMultipleRequestCompletionHandler)completionHandler { + if (requests.count == 0) { + return completionHandler(YES); + } + [requests enumerateObjectsUsingBlock:^(SDLRPCRequest * _Nonnull request, NSUInteger idx, BOOL * _Nonnull stop) { [self sendConnectionRequest:request withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) { if (progressHandler != nil) { @@ -67,6 +71,10 @@ - (void)sendRequests:(nonnull NSArray *)requests progressHandle } - (void)sendSequentialRequests:(nonnull NSArray *)requests progressHandler:(nullable SDLMultipleSequentialRequestProgressHandler)progressHandler completionHandler:(nullable SDLMultipleRequestCompletionHandler)completionHandler { + if (requests.count == 0) { + return completionHandler(YES); + } + [requests enumerateObjectsUsingBlock:^(SDLRPCRequest * _Nonnull request, NSUInteger idx, BOOL * _Nonnull stop) { [self sendConnectionRequest:request withResponseHandler:nil]; progressHandler(request, nil, nil, (double)idx / (double)requests.count);