diff options
author | Joel Fischer <joeljfischer@gmail.com> | 2020-12-09 11:55:56 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-12-09 11:55:56 -0500 |
commit | 039cf18f1632684117be59cba312630112bd9bd6 (patch) | |
tree | 2232a0765a27b9c7ddd3961efa5b87529d19d132 | |
parent | 361db38c0df94d69269358d55c34cc77a0102dac (diff) | |
parent | b6e606a94ff2e0042d9b5b4865799cfe6c7cde52 (diff) | |
download | sdl_ios-039cf18f1632684117be59cba312630112bd9bd6.tar.gz |
Merge pull request #1843 from smartdevicelink/feature/issue-1841-voice-command-manager
Refactor Voice Command Manager
17 files changed, 802 insertions, 236 deletions
diff --git a/SmartDeviceLink-iOS.xcodeproj/project.pbxproj b/SmartDeviceLink-iOS.xcodeproj/project.pbxproj index 948f32bfb..2773665f2 100644 --- a/SmartDeviceLink-iOS.xcodeproj/project.pbxproj +++ b/SmartDeviceLink-iOS.xcodeproj/project.pbxproj @@ -294,6 +294,7 @@ 4A402561250134CB0080E159 /* SDLStabilityControlsStatus.h in Headers */ = {isa = PBXBuildFile; fileRef = 4A40255F250134CA0080E159 /* SDLStabilityControlsStatus.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4A404C66250BBE11003AB65D /* SDLTextAndGraphicUpdateOperationSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A404C65250BBE11003AB65D /* SDLTextAndGraphicUpdateOperationSpec.m */; }; 4A404C68250BBE2B003AB65D /* SDLTextAndGraphicStateSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A404C67250BBE2B003AB65D /* SDLTextAndGraphicStateSpec.m */; }; + 4A41430D255F0A090039C267 /* TestConnectionRequestObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A41430C255F0A090039C267 /* TestConnectionRequestObject.m */; }; 4A457DC324A2933E00386CBA /* SDLLifecycleRPCAdapterSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A457DC224A2933E00386CBA /* SDLLifecycleRPCAdapterSpec.m */; }; 4A457DD324A3886700386CBA /* SDLLifecycleSyncPDataHandlerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A457DD224A3886700386CBA /* SDLLifecycleSyncPDataHandlerSpec.m */; }; 4A457DD524A3C16E00386CBA /* SDLLifecycleMobileHMIStateHandlerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A457DD424A3C16E00386CBA /* SDLLifecycleMobileHMIStateHandlerSpec.m */; }; @@ -1383,6 +1384,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, ); }; }; + 4ABED25B257681ED005BDF61 /* SDLVoiceCommandUpdateOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ABED259257681ED005BDF61 /* SDLVoiceCommandUpdateOperation.m */; }; + 4ABED25C257681ED005BDF61 /* SDLVoiceCommandUpdateOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 4ABED25A257681ED005BDF61 /* SDLVoiceCommandUpdateOperation.h */; }; + 4AD1F1742559957100637FE1 /* SDLVoiceCommandUpdateOperationSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4AD1F1732559957100637FE1 /* SDLVoiceCommandUpdateOperationSpec.m */; }; 4AE8A7022537796E000666C0 /* SmartDeviceLink.h in Headers */ = {isa = PBXBuildFile; fileRef = 4AE8A7012537796E000666C0 /* SmartDeviceLink.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5D0A9F911F15550400CC80DD /* SDLSystemCapabilityTypeSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D0A9F901F15550400CC80DD /* SDLSystemCapabilityTypeSpec.m */; }; 5D0A9F931F15560B00CC80DD /* SDLNavigationCapabilitySpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D0A9F921F15560B00CC80DD /* SDLNavigationCapabilitySpec.m */; }; @@ -2071,6 +2075,8 @@ 4A40255F250134CA0080E159 /* SDLStabilityControlsStatus.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDLStabilityControlsStatus.h; path = public/SDLStabilityControlsStatus.h; sourceTree = "<group>"; }; 4A404C65250BBE11003AB65D /* SDLTextAndGraphicUpdateOperationSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SDLTextAndGraphicUpdateOperationSpec.m; path = DevAPISpecs/SDLTextAndGraphicUpdateOperationSpec.m; sourceTree = "<group>"; }; 4A404C67250BBE2B003AB65D /* SDLTextAndGraphicStateSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLTextAndGraphicStateSpec.m; path = DevAPISpecs/SDLTextAndGraphicStateSpec.m; sourceTree = "<group>"; }; + 4A41430B255F0A090039C267 /* TestConnectionRequestObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestConnectionRequestObject.h; sourceTree = "<group>"; }; + 4A41430C255F0A090039C267 /* TestConnectionRequestObject.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestConnectionRequestObject.m; sourceTree = "<group>"; }; 4A457DC224A2933E00386CBA /* SDLLifecycleRPCAdapterSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLLifecycleRPCAdapterSpec.m; path = DevAPISpecs/SDLLifecycleRPCAdapterSpec.m; sourceTree = "<group>"; }; 4A457DD224A3886700386CBA /* SDLLifecycleSyncPDataHandlerSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLLifecycleSyncPDataHandlerSpec.m; path = DevAPISpecs/SDLLifecycleSyncPDataHandlerSpec.m; sourceTree = "<group>"; }; 4A457DD424A3C16E00386CBA /* SDLLifecycleMobileHMIStateHandlerSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLLifecycleMobileHMIStateHandlerSpec.m; path = DevAPISpecs/SDLLifecycleMobileHMIStateHandlerSpec.m; sourceTree = "<group>"; }; @@ -3166,6 +3172,9 @@ 4ABB2B9924F850AD0061BF55 /* SDLImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDLImage.h; path = public/SDLImage.h; sourceTree = "<group>"; }; 4ABB2B9A24F850AD0061BF55 /* SDLLightState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SDLLightState.m; path = public/SDLLightState.m; sourceTree = "<group>"; }; 4ABB2B9B24F850AD0061BF55 /* SDLImageResolution.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDLImageResolution.h; path = public/SDLImageResolution.h; sourceTree = "<group>"; }; + 4ABED259257681ED005BDF61 /* SDLVoiceCommandUpdateOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SDLVoiceCommandUpdateOperation.m; path = private/SDLVoiceCommandUpdateOperation.m; sourceTree = "<group>"; }; + 4ABED25A257681ED005BDF61 /* SDLVoiceCommandUpdateOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDLVoiceCommandUpdateOperation.h; path = private/SDLVoiceCommandUpdateOperation.h; sourceTree = "<group>"; }; + 4AD1F1732559957100637FE1 /* SDLVoiceCommandUpdateOperationSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDLVoiceCommandUpdateOperationSpec.m; path = DevAPISpecs/SDLVoiceCommandUpdateOperationSpec.m; sourceTree = "<group>"; }; 4AE8A7012537796E000666C0 /* SmartDeviceLink.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SmartDeviceLink.h; path = public/SmartDeviceLink.h; sourceTree = "<group>"; }; 4AE8A707253779F9000666C0 /* EAAccessory+OCMock.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "EAAccessory+OCMock.h"; sourceTree = "<group>"; }; 5D0A9F901F15550400CC80DD /* SDLSystemCapabilityTypeSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDLSystemCapabilityTypeSpec.m; sourceTree = "<group>"; }; @@ -4118,6 +4127,47 @@ path = MessageSpecs; sourceTree = "<group>"; }; + 4A32B3E425559D93001FFA26 /* Voice Cammands */ = { + isa = PBXGroup; + children = ( + 4A32B3E525559DA4001FFA26 /* Cells */, + 4A32B3E625559DAC001FFA26 /* Operations */, + 4ABB25A824F7E6E10061BF55 /* SDLVoiceCommandManager.h */, + 4ABB25A724F7E6E10061BF55 /* SDLVoiceCommandManager.m */, + ); + name = "Voice Cammands"; + sourceTree = "<group>"; + }; + 4A32B3E525559DA4001FFA26 /* Cells */ = { + isa = PBXGroup; + children = ( + 4ABB259124F7E6820061BF55 /* SDLVoiceCommand.h */, + 4ABB259024F7E6820061BF55 /* SDLVoiceCommand.m */, + ); + name = Cells; + sourceTree = "<group>"; + }; + 4A32B3E625559DAC001FFA26 /* Operations */ = { + isa = PBXGroup; + children = ( + 4ABED25A257681ED005BDF61 /* SDLVoiceCommandUpdateOperation.h */, + 4ABED259257681ED005BDF61 /* SDLVoiceCommandUpdateOperation.m */, + ); + name = Operations; + sourceTree = "<group>"; + }; + 4A32B3F325559F37001FFA26 /* Menu */ = { + isa = PBXGroup; + children = ( + 5D76751022D907F500E8D71A /* Configuration */, + 755F175E229F14F70041B9CB /* Dynamic Menu Update Utilities */, + 5D339CEC207C08AB000CC364 /* Cells */, + 4ABB25A924F7E6E10061BF55 /* SDLMenuManager.h */, + 4ABB25A624F7E6E10061BF55 /* SDLMenuManager.m */, + ); + name = Menu; + sourceTree = "<group>"; + }; 4A3BA4D9248E8EBB003E56B8 /* SystemRequest Handler */ = { isa = PBXGroup; children = ( @@ -4211,6 +4261,16 @@ name = "Status Manager"; sourceTree = "<group>"; }; + 4AD1F16A2559952D00637FE1 /* Voice Command */ = { + isa = PBXGroup; + children = ( + 5DF40B27208FDA9700DD6FDA /* SDLVoiceCommandManagerSpec.m */, + 5DAB5F5220989A8300A020C8 /* SDLVoiceCommandSpec.m */, + 4AD1F1732559957100637FE1 /* SDLVoiceCommandUpdateOperationSpec.m */, + ); + name = "Voice Command"; + sourceTree = "<group>"; + }; 5D0218EB1A8E795700D1BF62 /* UI */ = { isa = PBXGroup; children = ( @@ -4382,13 +4442,8 @@ 5D339CE5207C0651000CC364 /* Menu */ = { isa = PBXGroup; children = ( - 5D76751022D907F500E8D71A /* Configuration */, - 755F175E229F14F70041B9CB /* Dynamic Menu Update Utilities */, - 5D339CEC207C08AB000CC364 /* Cells */, - 4ABB25A924F7E6E10061BF55 /* SDLMenuManager.h */, - 4ABB25A624F7E6E10061BF55 /* SDLMenuManager.m */, - 4ABB25A824F7E6E10061BF55 /* SDLVoiceCommandManager.h */, - 4ABB25A724F7E6E10061BF55 /* SDLVoiceCommandManager.m */, + 4A32B3F325559F37001FFA26 /* Menu */, + 4A32B3E425559D93001FFA26 /* Voice Cammands */, ); name = Menu; sourceTree = "<group>"; @@ -4398,8 +4453,6 @@ children = ( 4ABB259424F7E6880061BF55 /* SDLMenuCell.h */, 4ABB259524F7E6880061BF55 /* SDLMenuCell.m */, - 4ABB259124F7E6820061BF55 /* SDLVoiceCommand.h */, - 4ABB259024F7E6820061BF55 /* SDLVoiceCommand.m */, ); name = Cells; sourceTree = "<group>"; @@ -4440,8 +4493,8 @@ children = ( 5D4019B11A76EC350006B0C2 /* Examples */, 5D61FA1D1A84237100846EE7 /* SmartDeviceLink */, - 5D61FA2C1A84237100846EE7 /* SmartDeviceLinkTests */, 5D4346621E6F38E600B639C6 /* SmartDeviceLinkSwift */, + 5D61FA2C1A84237100846EE7 /* SmartDeviceLinkTests */, 5D4019B01A76EC350006B0C2 /* Products */, ); sourceTree = "<group>"; @@ -6083,6 +6136,7 @@ 5DAD5F8220507DED0025624C /* Soft Button */, 88D0E5D42478656B009469AB /* Subscribe Button */, 5DAD5F8320507DF30025624C /* Text and Graphic */, + 4AD1F16A2559952D00637FE1 /* Voice Command */, 5DAD5F8420507E1F0025624C /* SDLScreenManagerSpec.m */, ); name = Screen; @@ -6153,6 +6207,8 @@ 8850DB5F1F4475D30053A48D /* TestMultipleFilesConnectionManager.m */, 5D6035D3202CE4A500A429C9 /* TestMultipleRequestsConnectionManager.h */, 5D6035D4202CE4A500A429C9 /* TestMultipleRequestsConnectionManager.m */, + 4A41430B255F0A090039C267 /* TestConnectionRequestObject.h */, + 4A41430C255F0A090039C267 /* TestConnectionRequestObject.m */, ); name = "Connection Manager"; sourceTree = "<group>"; @@ -6443,9 +6499,7 @@ isa = PBXGroup; children = ( 5DF40B25208FA7DE00DD6FDA /* SDLMenuManagerSpec.m */, - 5DF40B27208FDA9700DD6FDA /* SDLVoiceCommandManagerSpec.m */, 5DAB5F502098994C00A020C8 /* SDLMenuCellSpec.m */, - 5DAB5F5220989A8300A020C8 /* SDLVoiceCommandSpec.m */, 752ECDB8228C42E100D945F4 /* SDLMenuRunScoreSpec.m */, 752ECDBA228C532600D945F4 /* SDLMenuUpdateAlgorithmSpec.m */, 5D76751522D920FD00E8D71A /* SDLMenuConfigurationSpec.m */, @@ -7338,6 +7392,7 @@ 4ABB26BA24F7FA1C0061BF55 /* SDLLogConstants.h in Headers */, 4ABB28DB24F82A6A0061BF55 /* SDLOnSystemCapabilityUpdated.h in Headers */, 4ABB269324F7F9060061BF55 /* SDLTimer.h in Headers */, + 4ABED25C257681ED005BDF61 /* SDLVoiceCommandUpdateOperation.h in Headers */, 4A8BD2F924F93872000945E3 /* SDLVehicleDataType.h in Headers */, 4ABB279924F7FF0B0061BF55 /* SDLLanguage.h in Headers */, 4ABB285F24F828E00061BF55 /* SDLVehicleDataActiveStatus.h in Headers */, @@ -7543,7 +7598,7 @@ }; 5D61FA1B1A84237100846EE7 = { CreatedOnToolsVersion = 6.1.1; - LastSwiftMigration = 1170; + LastSwiftMigration = 1210; }; 5D61FA251A84237100846EE7 = { CreatedOnToolsVersion = 6.1.1; @@ -7700,6 +7755,7 @@ 4ABB264524F7F5340061BF55 /* SDLSystemCapabilityManager.m in Sources */, 4A8BD2B124F935BC000945E3 /* SDLSoftButtonCapabilities.m in Sources */, 4ABB2A5524F847B10061BF55 /* SDLGetInteriorVehicleDataConsentResponse.m in Sources */, + 4ABED25B257681ED005BDF61 /* SDLVoiceCommandUpdateOperation.m in Sources */, 4ABB2AA924F847F40061BF55 /* SDLSetAppIconResponse.m in Sources */, 4A8BD2D224F93803000945E3 /* SDLTemplateColorScheme.m in Sources */, 4A8BD3C124F994D5000945E3 /* SDLFileManager.m in Sources */, @@ -8302,6 +8358,7 @@ 162E83181A9BDE8B00906325 /* SDLOnKeyboardInputSpec.m in Sources */, 1EE8C4441F34A1B900FDC2CF /* SDLClimateControlDataSpec.m in Sources */, 162E83701A9BDE8B00906325 /* SDLUpdateTurnListResponseSpec.m in Sources */, + 4A41430D255F0A090039C267 /* TestConnectionRequestObject.m in Sources */, 88C23E8822297C6000EA171F /* SDLRPCResponseNotificationSpec.m in Sources */, 162E833B1A9BDE8B00906325 /* SDLSetGlobalPropertiesSpec.m in Sources */, 884AF94C220B3FCC00E22928 /* SDLGetSystemCapabilitySpec.m in Sources */, @@ -8696,6 +8753,7 @@ 162E831E1A9BDE8B00906325 /* SDLOnTBTClientStateSpec.m in Sources */, 162E83351A9BDE8B00906325 /* SDLReadDIDSpec.m in Sources */, 5DF40B28208FDA9700DD6FDA /* SDLVoiceCommandManagerSpec.m in Sources */, + 4AD1F1742559957100637FE1 /* SDLVoiceCommandUpdateOperationSpec.m in Sources */, 88B3BFA020DA8FD000943565 /* SDLFuelTypeSpec.m in Sources */, 162E836F1A9BDE8B00906325 /* SDLUnsubscribeVehicleDataResponseSpec.m in Sources */, 162E82DB1A9BDE8B00906325 /* SDLECallConfirmationStatusSpec.m in Sources */, diff --git a/SmartDeviceLink/private/SDLError.h b/SmartDeviceLink/private/SDLError.h index b7fb5997c..fae799fe9 100644 --- a/SmartDeviceLink/private/SDLError.h +++ b/SmartDeviceLink/private/SDLError.h @@ -55,6 +55,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark Menu Manager + (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 7000d49b8..a54cee83e 100644 --- a/SmartDeviceLink/private/SDLError.m +++ b/SmartDeviceLink/private/SDLError.m @@ -262,6 +262,13 @@ NS_ASSUME_NONNULL_BEGIN return [NSError errorWithDomain:SDLErrorDomainMenuManager code:SDLMenuManagerErrorRPCsFailed userInfo:userInfo]; } ++ (NSError *)sdl_voiceCommandManager_pendingUpdateSuperseded { + return [NSError errorWithDomain:SDLErrorDomainMenuManager code:SDLMenuManagerErrorPendingUpdateSuperseded userInfo:@{ + NSLocalizedDescriptionKey: @"Voice Command Manager error", + NSLocalizedFailureReasonErrorKey: @"Voice command operation was cancelled because it was superseded by another update" + }]; +} + #pragma mark Choice Set Manager + (NSError *)sdl_choiceSetManager_choicesDeletedBeforePresentation:(NSDictionary *)userInfo { diff --git a/SmartDeviceLink/private/SDLLogFileModuleMap.m b/SmartDeviceLink/private/SDLLogFileModuleMap.m index 403a82258..29834a1d0 100644 --- a/SmartDeviceLink/private/SDLLogFileModuleMap.m +++ b/SmartDeviceLink/private/SDLLogFileModuleMap.m @@ -130,7 +130,7 @@ } + (SDLLogFileModule *)sdl_screenManagerMenuModule { - return [SDLLogFileModule moduleWithName:@"Screen/Menu" files:[NSSet setWithArray:@[@"SDLMenuManager", @"SDLVoiceCommandManager"]]]; + return [SDLLogFileModule moduleWithName:@"Screen/Menu" files:[NSSet setWithArray:@[@"SDLMenuManager", @"SDLVoiceCommandManager", @"SDLVoiceCommandUpdateOperation"]]]; } + (SDLLogFileModule *)sdl_screenManagerChoiceSetModule { diff --git a/SmartDeviceLink/private/SDLVoiceCommandManager.h b/SmartDeviceLink/private/SDLVoiceCommandManager.h index 8c3f3f78f..1bd9a6567 100644 --- a/SmartDeviceLink/private/SDLVoiceCommandManager.h +++ b/SmartDeviceLink/private/SDLVoiceCommandManager.h @@ -15,13 +15,6 @@ NS_ASSUME_NONNULL_BEGIN -/** - The handler run when the update has completed - - @param error An error if the update failed and an error occurred - */ -typedef void(^SDLMenuUpdateCompletionHandler)(NSError *__nullable error); - @interface SDLVoiceCommandManager : NSObject - (instancetype)initWithConnectionManager:(id<SDLConnectionManagerType>)connectionManager; diff --git a/SmartDeviceLink/private/SDLVoiceCommandManager.m b/SmartDeviceLink/private/SDLVoiceCommandManager.m index e9bd3d5e6..fc0048736 100644 --- a/SmartDeviceLink/private/SDLVoiceCommandManager.m +++ b/SmartDeviceLink/private/SDLVoiceCommandManager.m @@ -21,6 +21,7 @@ #import "SDLRPCNotificationNotification.h" #import "SDLRPCRequest.h" #import "SDLVoiceCommand.h" +#import "SDLVoiceCommandUpdateOperation.h" NS_ASSUME_NONNULL_BEGIN @@ -33,15 +34,12 @@ NS_ASSUME_NONNULL_BEGIN @interface SDLVoiceCommandManager() @property (weak, nonatomic) id<SDLConnectionManagerType> connectionManager; +@property (strong, nonatomic) NSOperationQueue *transactionQueue; -@property (assign, nonatomic) BOOL waitingOnHMIUpdate; -@property (copy, nonatomic, nullable) SDLHMILevel currentHMILevel; - -@property (strong, nonatomic, nullable) NSArray<SDLRPCRequest *> *inProgressUpdate; -@property (assign, nonatomic) BOOL hasQueuedUpdate; +@property (copy, nonatomic, nullable) SDLHMILevel currentLevel; @property (assign, nonatomic) UInt32 lastVoiceCommandId; -@property (copy, nonatomic) NSArray<SDLVoiceCommand *> *oldVoiceCommands; +@property (copy, nonatomic) NSArray<SDLVoiceCommand *> *currentVoiceCommands; @end @@ -54,8 +52,9 @@ UInt32 const VoiceCommandIdMin = 1900000000; if (!self) { return nil; } _lastVoiceCommandId = VoiceCommandIdMin; + _transactionQueue = [self sdl_newTransactionQueue]; _voiceCommands = @[]; - _oldVoiceCommands = @[]; + _currentVoiceCommands = @[]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sdl_hmiStatusNotification:) name:SDLDidChangeHMIStatusNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sdl_commandNotification:) name:SDLDidReceiveCommandNotification object:nil]; @@ -75,120 +74,70 @@ UInt32 const VoiceCommandIdMin = 1900000000; - (void)stop { _lastVoiceCommandId = VoiceCommandIdMin; _voiceCommands = @[]; - _oldVoiceCommands = @[]; + _currentVoiceCommands = @[]; + _transactionQueue = [self sdl_newTransactionQueue]; + + _currentLevel = nil; +} + +- (NSOperationQueue *)sdl_newTransactionQueue { + NSOperationQueue *queue = [[NSOperationQueue alloc] init]; + queue.name = @"SDLVoiceCommandManager Transaction Queue"; + queue.maxConcurrentOperationCount = 1; + queue.qualityOfService = NSQualityOfServiceUserInitiated; + queue.suspended = YES; - _waitingOnHMIUpdate = NO; - _currentHMILevel = nil; - _inProgressUpdate = nil; - _hasQueuedUpdate = NO; + 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.currentLevel isEqualToEnum:SDLHMILevelNone]) { + SDLLogD(@"Suspending the transaction queue. Current HMI level is NONE: %@", ([self.currentLevel isEqualToEnum:SDLHMILevelNone] ? @"YES" : @"NO")); + self.transactionQueue.suspended = YES; + } else { + SDLLogD(@"Starting the transaction queue"); + self.transactionQueue.suspended = NO; + } } #pragma mark - Setters - (void)setVoiceCommands:(NSArray<SDLVoiceCommand *> *)voiceCommands { - if (self.currentHMILevel == nil || [self.currentHMILevel isEqualToEnum:SDLHMILevelNone]) { - self.waitingOnHMIUpdate = YES; + if (voiceCommands == self.voiceCommands) { + SDLLogD(@"New voice commands are equal to the existing voice commands, skipping..."); return; } - self.waitingOnHMIUpdate = NO; - // Set the ids - self.lastVoiceCommandId = VoiceCommandIdMin; [self sdl_updateIdsOnVoiceCommands:voiceCommands]; - _oldVoiceCommands = _voiceCommands; + // Set the new voice commands internally _voiceCommands = voiceCommands; - [self sdl_updateWithCompletionHandler:nil]; -} - -#pragma mark - Updating System - -- (void)sdl_updateWithCompletionHandler:(nullable SDLMenuUpdateCompletionHandler)completionHandler { - if (self.currentHMILevel == nil || [self.currentHMILevel isEqualToEnum:SDLHMILevelNone]) { - self.waitingOnHMIUpdate = YES; - return; - } - - if (self.inProgressUpdate != nil) { - // There's an in progress update, we need to put this on hold - self.hasQueuedUpdate = YES; - return; - } - - __weak typeof(self) weakself = self; - [self sdl_sendDeleteCurrentVoiceCommands:^(NSError * _Nullable error) { - [weakself sdl_sendCurrentVoiceCommands:^(NSError * _Nullable error) { - weakself.inProgressUpdate = nil; - - if (completionHandler != nil) { - completionHandler(error); - } - - if (weakself.hasQueuedUpdate) { - [weakself sdl_updateWithCompletionHandler:nil]; - weakself.hasQueuedUpdate = NO; - } - }]; + // Create the operation, cancel previous ones and set this one + __weak typeof(self) weakSelf = self; + SDLVoiceCommandUpdateOperation *updateOperation = [[SDLVoiceCommandUpdateOperation alloc] initWithConnectionManager:self.connectionManager pendingVoiceCommands:voiceCommands oldVoiceCommands:_currentVoiceCommands updateCompletionHandler:^(NSArray<SDLVoiceCommand *> *newCurrentVoiceCommands, NSError * _Nullable error) { + weakSelf.currentVoiceCommands = newCurrentVoiceCommands; + [weakSelf sdl_updatePendingOperationsWithNewCurrentVoiceCommands:newCurrentVoiceCommands]; }]; -} - -#pragma mark Delete Old Menu Items - -- (void)sdl_sendDeleteCurrentVoiceCommands:(SDLMenuUpdateCompletionHandler)completionHandler { - if (self.oldVoiceCommands.count == 0) { - completionHandler(nil); - - return; - } - - NSArray<SDLRPCRequest *> *deleteVoiceCommands = [self sdl_deleteCommandsForVoiceCommands:self.oldVoiceCommands]; - self.oldVoiceCommands = @[]; - [self.connectionManager sendRequests:deleteVoiceCommands progressHandler:nil completionHandler:^(BOOL success) { - if (!success) { - SDLLogE(@"Error deleting old voice commands"); - } else { - SDLLogD(@"Finished deleting old voice commands"); - } - completionHandler(nil); - }]; + [self.transactionQueue cancelAllOperations]; + [self.transactionQueue addOperation:updateOperation]; } -#pragma mark Send New Menu Items - -- (void)sdl_sendCurrentVoiceCommands:(SDLMenuUpdateCompletionHandler)completionHandler { - if (self.voiceCommands.count == 0) { - SDLLogD(@"No voice commands to send"); - completionHandler(nil); +/// Update currently pending operations with a new set of "current" voice commands (the current state of the head unit) based on a previous completed operation +/// @param currentVoiceCommands The new current voice commands +- (void)sdl_updatePendingOperationsWithNewCurrentVoiceCommands:(NSArray<SDLVoiceCommand *> *)currentVoiceCommands { + for (NSOperation *operation in self.transactionQueue.operations) { + if (operation.isExecuting) { continue; } - return; + SDLVoiceCommandUpdateOperation *updateOp = (SDLVoiceCommandUpdateOperation *)operation; + updateOp.oldVoiceCommands = currentVoiceCommands; } - - self.inProgressUpdate = [self sdl_addCommandsForVoiceCommands:self.voiceCommands]; - - __block NSMutableDictionary<SDLRPCRequest *, NSError *> *errors = [NSMutableDictionary dictionary]; - __weak typeof(self) weakSelf = self; - [self.connectionManager sendRequests:self.inProgressUpdate 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 main menu commands: %@", errors); - completionHandler([NSError sdl_menuManager_failedToUpdateWithDictionary:errors]); - return; - } - - SDLLogD(@"Finished updating voice commands"); - weakSelf.oldVoiceCommands = weakSelf.voiceCommands; - completionHandler(nil); - }]; } #pragma mark - Helpers - #pragma mark IDs - (void)sdl_updateIdsOnVoiceCommands:(NSArray<SDLVoiceCommand *> *)voiceCommands { @@ -197,37 +146,6 @@ UInt32 const VoiceCommandIdMin = 1900000000; } } -#pragma mark Deletes - -- (NSArray<SDLDeleteCommand *> *)sdl_deleteCommandsForVoiceCommands:(NSArray<SDLVoiceCommand *> *)voiceCommands { - NSMutableArray<SDLDeleteCommand *> *mutableDeletes = [NSMutableArray array]; - for (SDLVoiceCommand *command in voiceCommands) { - SDLDeleteCommand *delete = [[SDLDeleteCommand alloc] initWithId:command.commandId]; - [mutableDeletes addObject:delete]; - } - - return [mutableDeletes copy]; -} - -#pragma mark Commands - -- (NSArray<SDLAddCommand *> *)sdl_addCommandsForVoiceCommands:(NSArray<SDLVoiceCommand *> *)voiceCommands { - NSMutableArray<SDLAddCommand *> *mutableCommands = [NSMutableArray array]; - for (SDLVoiceCommand *command in voiceCommands) { - [mutableCommands addObject:[self sdl_commandForVoiceCommand:command]]; - } - - return [mutableCommands copy]; -} - -- (SDLAddCommand *)sdl_commandForVoiceCommand:(SDLVoiceCommand *)voiceCommand { - SDLAddCommand *command = [[SDLAddCommand alloc] init]; - command.vrCommands = voiceCommand.voiceCommands; - command.cmdID = @(voiceCommand.commandId); - - return command; -} - #pragma mark - Observers - (void)sdl_commandNotification:(SDLRPCNotificationNotification *)notification { @@ -243,22 +161,12 @@ UInt32 const VoiceCommandIdMin = 1900000000; - (void)sdl_hmiStatusNotification:(SDLRPCNotificationNotification *)notification { SDLOnHMIStatus *hmiStatus = (SDLOnHMIStatus *)notification.notification; - if (hmiStatus.windowID != nil && hmiStatus.windowID.integerValue != SDLPredefinedWindowsDefaultWindow) { return; } - - SDLHMILevel oldHMILevel = self.currentHMILevel; - self.currentHMILevel = hmiStatus.hmiLevel; - - // Auto-send an updated show if we were in NONE and now we are not - if ([oldHMILevel isEqualToEnum:SDLHMILevelNone] && ![self.currentHMILevel isEqualToEnum:SDLHMILevelNone]) { - if (self.waitingOnHMIUpdate) { - [self setVoiceCommands:_voiceCommands]; - } else { - [self sdl_updateWithCompletionHandler:nil]; - } - } + + self.currentLevel = hmiStatus.hmiLevel; + [self sdl_updateTransactionQueueSuspended]; } @end diff --git a/SmartDeviceLink/private/SDLVoiceCommandUpdateOperation.h b/SmartDeviceLink/private/SDLVoiceCommandUpdateOperation.h new file mode 100644 index 000000000..ec0cf86ec --- /dev/null +++ b/SmartDeviceLink/private/SDLVoiceCommandUpdateOperation.h @@ -0,0 +1,38 @@ +// +// SDLVoiceCommandUpdateOperation.h +// SmartDeviceLink +// +// Created by Joel Fischer on 11/6/20. +// Copyright © 2020 smartdevicelink. All rights reserved. +// + +#import "SDLAsynchronousOperation.h" + +@class SDLVoiceCommand; + +@protocol SDLConnectionManagerType; + +NS_ASSUME_NONNULL_BEGIN + +/// A handler run when the operation completes, containing the new voice commands that are available on the head unit +/// +/// @param newCurrentVoiceCommands The voice commands currently present on the head unit +/// @param error The error if one occurred +typedef void(^SDLVoiceCommandUpdateCompletionHandler)(NSArray<SDLVoiceCommand *> *newCurrentVoiceCommands, NSError *__nullable error); + +/// An operation that handles changing voice commands on the head unit +@interface SDLVoiceCommandUpdateOperation : SDLAsynchronousOperation + +/// The voice commands currently on the head unit +@property (strong, nonatomic, nullable) NSArray<SDLVoiceCommand *> *oldVoiceCommands; + +/// Initialize a voice command update operation +/// @param connectionManager The connection manager for sending RPCs +/// @param pendingVoiceCommands The voice commands that should be on the head unit when this operation completes +/// @param oldVoiceCommands The voice commands currently on the head unit +/// @param completionHandler A handler called right before the update operation finishes +- (instancetype)initWithConnectionManager:(id<SDLConnectionManagerType>)connectionManager pendingVoiceCommands:(NSArray<SDLVoiceCommand *> *)pendingVoiceCommands oldVoiceCommands:(NSArray<SDLVoiceCommand *> *)oldVoiceCommands updateCompletionHandler:(SDLVoiceCommandUpdateCompletionHandler)completionHandler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLink/private/SDLVoiceCommandUpdateOperation.m b/SmartDeviceLink/private/SDLVoiceCommandUpdateOperation.m new file mode 100644 index 000000000..184615d63 --- /dev/null +++ b/SmartDeviceLink/private/SDLVoiceCommandUpdateOperation.m @@ -0,0 +1,227 @@ +// +// SDLVoiceCommandUpdateOperation.m +// SmartDeviceLink +// +// Created by Joel Fischer on 11/6/20. +// Copyright © 2020 smartdevicelink. All rights reserved. +// + +#import "SDLVoiceCommandUpdateOperation.h" + +#import "SDLAddCommand.h" +#import "SDLConnectionManagerType.h" +#import "SDLDeleteCommand.h" +#import "SDLError.h" +#import "SDLLogMacros.h" +#import "SDLVoiceCommand.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface SDLVoiceCommand() + +@property (assign, nonatomic) UInt32 commandId; + +@end + +@interface SDLVoiceCommandUpdateOperation () + +@property (weak, nonatomic) id<SDLConnectionManagerType> connectionManager; +@property (copy, nonatomic) NSArray<SDLVoiceCommand *> *pendingVoiceCommands; +@property (strong, nonatomic) NSMutableArray<SDLVoiceCommand *> *currentVoiceCommands; +@property (copy, nonatomic) SDLVoiceCommandUpdateCompletionHandler completionHandler; + +@property (copy, nonatomic, nullable) NSError *internalError; + +@end + +@implementation SDLVoiceCommandUpdateOperation + +- (instancetype)initWithConnectionManager:(id<SDLConnectionManagerType>)connectionManager pendingVoiceCommands:(NSArray<SDLVoiceCommand *> *)pendingVoiceCommands oldVoiceCommands:(NSArray<SDLVoiceCommand *> *)oldVoiceCommands updateCompletionHandler:(SDLVoiceCommandUpdateCompletionHandler)completionHandler { + self = [self init]; + if (!self) { return nil; } + + _connectionManager = connectionManager; + _pendingVoiceCommands = pendingVoiceCommands; + _oldVoiceCommands = oldVoiceCommands; + _currentVoiceCommands = oldVoiceCommands.mutableCopy; + _completionHandler = completionHandler; + + return self; +} + +- (void)start { + [super start]; + if (self.isCancelled) { + [self finishOperation]; + return; + } + + __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; + } + + // Send the new commands + [weakSelf sdl_sendCurrentVoiceCommands:^{ + [weakSelf finishOperation]; + }]; + }]; +} + +#pragma mark - Sending RPCs + +/// Send DeleteCommand RPCs for voice commands that should be deleted +/// @param completionHandler A handler called when all DeleteCommands have completed +- (void)sdl_sendDeleteCurrentVoiceCommands:(void(^)(void))completionHandler { + if (self.oldVoiceCommands.count == 0) { + SDLLogD(@"No voice commands to delete"); + return completionHandler(); + } + + NSArray<SDLDeleteCommand *> *deleteVoiceCommands = [self sdl_deleteCommandsForVoiceCommands:self.oldVoiceCommands]; + __block NSMutableDictionary<SDLDeleteCommand *, NSError *> *errors = [NSMutableDictionary dictionary]; + __weak typeof(self) weakSelf = self; + [self.connectionManager sendRequests:deleteVoiceCommands progressHandler:^(__kindof SDLRPCRequest * _Nonnull request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error, float percentComplete) { + // Add the request to the error dict or remove it from the current voice commands array + if (error != nil) { + errors[request] = error; + } else { + [weakSelf sdl_removeCurrentVoiceCommandWithCommandId:((SDLDeleteCommand *)request).cmdID.unsignedIntValue]; + } + + SDLLogV(@"Deleting voice commands progress: %f", percentComplete); + } completionHandler:^(BOOL success) { + if (!success) { + SDLLogE(@"Failed to send voice commands: %@", errors.allKeys); + SDLLogE(@"Failure reasons: %@", errors.allValues); + weakSelf.internalError = [NSError sdl_menuManager_failedToUpdateWithDictionary:errors]; + return completionHandler(); + } + + SDLLogD(@"Finished deleting old voice commands"); + return completionHandler(); + }]; +} + +/// Send AddCommand RPCs for voice commands that should be added +/// @param completionHandler A handler called when all AddCommands have completed +- (void)sdl_sendCurrentVoiceCommands:(void(^)(void))completionHandler { + if (self.pendingVoiceCommands.count == 0) { + SDLLogD(@"No voice commands to send"); + return completionHandler(); + } + + NSArray<SDLAddCommand *> *addCommandsToSend = [self sdl_addCommandsForVoiceCommands:self.pendingVoiceCommands]; + __block NSMutableDictionary<SDLAddCommand *, NSError *> *errors = [NSMutableDictionary dictionary]; + __weak typeof(self) weakSelf = self; + [self.connectionManager sendRequests:addCommandsToSend progressHandler:^(__kindof SDLRPCRequest * _Nonnull request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error, float percentComplete) { + // Add the request to the error dict or add it to the current voice commands array + if (error != nil) { + errors[request] = error; + } else { + [weakSelf.currentVoiceCommands addObject:[weakSelf sdl_pendingVoiceCommandWithCommandId:((SDLAddCommand *)request).cmdID.unsignedIntValue]]; + } + + SDLLogV(@"Sending voice commands progress: %f", percentComplete); + } completionHandler:^(BOOL success) { + if (!success) { + SDLLogE(@"Failed to send voice commands: %@", errors.allKeys); + SDLLogE(@"Failure reasons: %@", errors.allValues); + weakSelf.internalError = [NSError sdl_menuManager_failedToUpdateWithDictionary:errors]; + return completionHandler(); + } + + SDLLogD(@"Finished updating voice commands"); + return completionHandler(); + }]; +} + +#pragma mark - Helpers +#pragma mark Deletes + +/// Create DeleteCommand RPCs for passed voice commands +/// @param voiceCommands The voice commands that should be deleted +/// @return The DeleteCommand RPCs for the passed voice commands +- (NSArray<SDLDeleteCommand *> *)sdl_deleteCommandsForVoiceCommands:(NSArray<SDLVoiceCommand *> *)voiceCommands { + NSMutableArray<SDLDeleteCommand *> *mutableDeletes = [NSMutableArray array]; + for (SDLVoiceCommand *command in voiceCommands) { + SDLDeleteCommand *delete = [[SDLDeleteCommand alloc] initWithId:command.commandId]; + [mutableDeletes addObject:delete]; + } + + return [mutableDeletes copy]; +} + +#pragma mark Commands + +/// Create AddCommand RPCs for passed voice commands +/// @param voiceCommands The voice commands that should be added +/// @return The AddCommand RPCs for the passed voice commands +- (NSArray<SDLAddCommand *> *)sdl_addCommandsForVoiceCommands:(NSArray<SDLVoiceCommand *> *)voiceCommands { + NSMutableArray<SDLAddCommand *> *mutableCommands = [NSMutableArray array]; + for (SDLVoiceCommand *voiceCommand in voiceCommands) { + SDLAddCommand *command = [[SDLAddCommand alloc] init]; + command.vrCommands = voiceCommand.voiceCommands; + command.cmdID = @(voiceCommand.commandId); + + [mutableCommands addObject:command]; + } + + return [mutableCommands copy]; +} + +#pragma mark - Managing list of commands on head unit + +/// Remove a voice command from the array of voice commands on the head unit based on a command id +/// @param commandId The command id to use to remove a voice command +- (void)sdl_removeCurrentVoiceCommandWithCommandId:(UInt32)commandId { + for (SDLVoiceCommand *voiceCommand in self.currentVoiceCommands) { + if (voiceCommand.commandId == commandId) { + [self.currentVoiceCommands removeObject:voiceCommand]; + break; + } + } +} + +/// Find a voice command in the pending voice command array based on a command id +/// @param commandId The command id to use to find a voice command +- (nullable SDLVoiceCommand *)sdl_pendingVoiceCommandWithCommandId:(UInt32)commandId { + for (SDLVoiceCommand *voiceCommand in self.pendingVoiceCommands) { + if (voiceCommand.commandId == commandId) { + return voiceCommand; + } + } + + return nil; +} + +#pragma mark - Operation Overrides + +- (void)finishOperation { + SDLLogV(@"Finishing voice command update operation"); + if (self.isCancelled) { + self.internalError = [NSError sdl_voiceCommandManager_pendingUpdateSuperseded]; + } + + self.completionHandler(self.currentVoiceCommands.copy, self.error); + [super finishOperation]; +} + +- (nullable NSString *)name { + return @"com.sdl.voicecommand.update"; +} + +- (NSOperationQueuePriority)queuePriority { + return NSOperationQueuePriorityNormal; +} + +- (nullable NSError *)error { + return self.internalError; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLink/public/SDLErrorConstants.h b/SmartDeviceLink/public/SDLErrorConstants.h index ea594ceff..12bb1c235 100644 --- a/SmartDeviceLink/public/SDLErrorConstants.h +++ b/SmartDeviceLink/public/SDLErrorConstants.h @@ -186,7 +186,8 @@ typedef NS_ENUM(NSInteger, SDLSubscribeButtonManagerError) { */ typedef NS_ENUM(NSInteger, SDLMenuManagerError) { /// Sending menu-related RPCs returned an error from the remote system - SDLMenuManagerErrorRPCsFailed = -1 + SDLMenuManagerErrorRPCsFailed = -1, + SDLMenuManagerErrorPendingUpdateSuperseded = -2 }; /// Errors associated with Choice Set class diff --git a/SmartDeviceLink/public/SDLScreenManager.h b/SmartDeviceLink/public/SDLScreenManager.h index bd1c9597f..5da5a1cbe 100644 --- a/SmartDeviceLink/public/SDLScreenManager.h +++ b/SmartDeviceLink/public/SDLScreenManager.h @@ -197,6 +197,8 @@ If set to `SDLDynamicMenuUpdatesModeForceOff`, menu updates will work the legacy /** The current list of voice commands available for the user to speak and be recognized by the IVI's voice recognition engine. + + @warning May not be an empty string */ @property (copy, nonatomic) NSArray<SDLVoiceCommand *> *voiceCommands; diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLLifecycleMobileHMIStateHandlerSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLLifecycleMobileHMIStateHandlerSpec.m index f2ff80c99..505edd4d5 100644 --- a/SmartDeviceLinkTests/DevAPISpecs/SDLLifecycleMobileHMIStateHandlerSpec.m +++ b/SmartDeviceLinkTests/DevAPISpecs/SDLLifecycleMobileHMIStateHandlerSpec.m @@ -71,7 +71,7 @@ describe(@"SDLLifecycleMobileHMIStateHandler tests", ^{ describe(@"after the manager is stopped", ^{ beforeEach(^{ - [mockConnectionManager.receivedRequests removeAllObjects]; + [mockConnectionManager reset]; [testManager stop]; }); diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLVoiceCommandManagerSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLVoiceCommandManagerSpec.m index c5e275843..8b808273e 100644 --- a/SmartDeviceLinkTests/DevAPISpecs/SDLVoiceCommandManagerSpec.m +++ b/SmartDeviceLinkTests/DevAPISpecs/SDLVoiceCommandManagerSpec.m @@ -7,10 +7,20 @@ #import "SDLDeleteCommand.h" #import "SDLFileManager.h" #import "SDLHMILevel.h" +#import "SDLOnHMIStatus.h" +#import "SDLPredefinedWindows.h" +#import "SDLRPCNotificationNotification.h" #import "SDLVoiceCommand.h" #import "SDLVoiceCommandManager.h" +#import "SDLVoiceCommandUpdateOperation.h" #import "TestConnectionManager.h" +@interface SDLVoiceCommandUpdateOperation () + +@property (strong, nonatomic) NSMutableArray<SDLVoiceCommand *> *currentVoiceCommands; + +@end + @interface SDLVoiceCommand() @property (assign, nonatomic) UInt32 commandId; @@ -21,14 +31,11 @@ @property (weak, nonatomic) id<SDLConnectionManagerType> connectionManager; -@property (assign, nonatomic) BOOL waitingOnHMIUpdate; -@property (copy, nonatomic, nullable) SDLHMILevel currentHMILevel; - -@property (strong, nonatomic, nullable) NSArray<SDLRPCRequest *> *inProgressUpdate; -@property (assign, nonatomic) BOOL hasQueuedUpdate; +@property (strong, nonatomic) NSOperationQueue *transactionQueue; +@property (copy, nonatomic, nullable) SDLHMILevel currentLevel; @property (assign, nonatomic) UInt32 lastVoiceCommandId; -@property (copy, nonatomic) NSArray<SDLVoiceCommand *> *oldVoiceCommands; +@property (copy, nonatomic) NSArray<SDLVoiceCommand *> *currentVoiceCommands; @end @@ -42,104 +49,126 @@ describe(@"voice command manager", ^{ __block SDLVoiceCommand *testVoiceCommand = [[SDLVoiceCommand alloc] initWithVoiceCommands:@[@"Test 1"] handler:^{}]; __block SDLVoiceCommand *testVoiceCommand2 = [[SDLVoiceCommand alloc] initWithVoiceCommands:@[@"Test 2"] handler:^{}]; + __block SDLOnHMIStatus *newHMIStatus = [[SDLOnHMIStatus alloc] init]; + __block NSArray<SDLVoiceCommand *> *testVCArray = nil; beforeEach(^{ + testVCArray = @[testVoiceCommand]; mockConnectionManager = [[TestConnectionManager alloc] init]; testManager = [[SDLVoiceCommandManager alloc] initWithConnectionManager:mockConnectionManager]; }); + // should instantiate correctly it(@"should instantiate correctly", ^{ expect(testManager.connectionManager).to(equal(mockConnectionManager)); - + expect(testManager.currentLevel).to(beNil()); expect(testManager.voiceCommands).to(beEmpty()); - expect(testManager.connectionManager).to(equal(mockConnectionManager)); - expect(testManager.currentHMILevel).to(beNil()); - expect(testManager.inProgressUpdate).to(beNil()); - expect(testManager.hasQueuedUpdate).to(beFalse()); - expect(testManager.waitingOnHMIUpdate).to(beFalse()); expect(testManager.lastVoiceCommandId).to(equal(VoiceCommandIdMin)); - expect(testManager.oldVoiceCommands).to(beEmpty()); + expect(testManager.currentVoiceCommands).to(beEmpty()); + expect(testManager.transactionQueue).toNot(beNil()); }); + // updating voice commands before HMI is ready describe(@"updating voice commands before HMI is ready", ^{ + + // when in HMI NONE context(@"when in HMI NONE", ^{ beforeEach(^{ - testManager.currentHMILevel = SDLHMILevelNone; + newHMIStatus.hmiLevel = SDLHMILevelNone; + newHMIStatus.windowID = @(SDLPredefinedWindowsDefaultWindow); + + SDLRPCNotificationNotification *notification = [[SDLRPCNotificationNotification alloc] initWithName:SDLDidChangeHMIStatusNotification object:nil rpcNotification:newHMIStatus]; + [[NSNotificationCenter defaultCenter] postNotification:notification]; }); it(@"should not update", ^{ testManager.voiceCommands = @[testVoiceCommand]; - expect(testManager.inProgressUpdate).to(beNil()); + expect(testManager.transactionQueue.isSuspended).to(beTrue()); + expect(testManager.transactionQueue.operationCount).to(equal(1)); }); }); + // when no HMI level has been received context(@"when no HMI level has been received", ^{ - beforeEach(^{ - testManager.currentHMILevel = nil; - }); - it(@"should not update", ^{ testManager.voiceCommands = @[testVoiceCommand]; - expect(testManager.inProgressUpdate).to(beNil()); + expect(testManager.transactionQueue.isSuspended).to(beTrue()); + expect(testManager.transactionQueue.operationCount).to(equal(1)); }); }); }); - describe(@"updating voice commands", ^{ + // updating voice commands + describe(@"when voice commands are set", ^{ beforeEach(^{ - testManager.currentHMILevel = SDLHMILevelFull; + newHMIStatus.hmiLevel = SDLHMILevelFull; + newHMIStatus.windowID = @(SDLPredefinedWindowsDefaultWindow); + + SDLRPCNotificationNotification *notification = [[SDLRPCNotificationNotification alloc] initWithName:SDLDidChangeHMIStatusNotification object:nil rpcNotification:newHMIStatus]; + [[NSNotificationCenter defaultCenter] postNotification:notification]; + + testManager.voiceCommands = testVCArray; }); + // should properly update a command it(@"should properly update a command", ^{ - testManager.voiceCommands = @[testVoiceCommand]; + expect(testManager.voiceCommands.firstObject.commandId).to(equal(VoiceCommandIdMin)); + expect(testManager.transactionQueue.isSuspended).to(beFalse()); + expect(testManager.transactionQueue.operations).to(haveCount(1)); + }); - NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLDeleteCommand class]]; - NSArray *deletes = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; - expect(deletes).to(beEmpty()); + // when new voice commands is identical to the existing ones + describe(@"when new voice commands is identical to the existing ones", ^{ + beforeEach(^{ + testManager.voiceCommands = testVCArray; + }); - NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", [SDLAddCommand class]]; - NSArray *add = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; - expect(add).toNot(beEmpty()); + // should only have one operation + it(@"should only have one operation", ^{ + expect(testManager.transactionQueue.operations).to(haveCount(1)); + }); }); - context(@"when a menu already exists", ^{ + // when new voice commands are set + describe(@"when new voice commands are set", ^{ beforeEach(^{ - testManager.voiceCommands = @[testVoiceCommand]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; // Add - testManager.voiceCommands = @[testVoiceCommand2]; - [mockConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; // Delete }); - it(@"should send deletes first", ^{ - NSPredicate *deleteCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLDeleteCommand class]]; - NSArray *deletes = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:deleteCommandPredicate]; + // should queue another operation + it(@"should queue another operation", ^{ + expect(testManager.transactionQueue.operations).to(haveCount(2)); + }); + + // when the first operation finishes and updates the current voice commands + describe(@"when the first operation finishes and updates the current voice commands", ^{ + beforeEach(^{ + SDLVoiceCommandUpdateOperation *firstOp = testManager.transactionQueue.operations[0]; + firstOp.currentVoiceCommands = [@[testVoiceCommand2] mutableCopy]; + [firstOp finishOperation]; + }); - NSPredicate *addCommandPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass:%@", [SDLAddCommand class]]; - NSArray *adds = [[mockConnectionManager.receivedRequests copy] filteredArrayUsingPredicate:addCommandPredicate]; + it(@"should update the second operation", ^{ + SDLVoiceCommandUpdateOperation *secondOp = testManager.transactionQueue.operations[0]; - expect(deletes).to(haveCount(1)); - expect(adds).to(haveCount(2)); + expect(secondOp.oldVoiceCommands.firstObject).to(equal(testVoiceCommand2)); + }); }); }); }); - context(@"On disconnects", ^{ + // on disconnect + context(@"on disconnect", ^{ beforeEach(^{ [testManager stop]; }); it(@"should reset correctly", ^{ expect(testManager.connectionManager).to(equal(mockConnectionManager)); - expect(testManager.voiceCommands).to(beEmpty()); - expect(testManager.connectionManager).to(equal(mockConnectionManager)); - expect(testManager.currentHMILevel).to(beNil()); - expect(testManager.inProgressUpdate).to(beNil()); - expect(testManager.hasQueuedUpdate).to(beFalse()); - expect(testManager.waitingOnHMIUpdate).to(beFalse()); + expect(testManager.currentLevel).to(beNil()); expect(testManager.lastVoiceCommandId).to(equal(VoiceCommandIdMin)); - expect(testManager.oldVoiceCommands).to(beEmpty()); + expect(testManager.currentVoiceCommands).to(beEmpty()); }); }); }); diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLVoiceCommandUpdateOperationSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLVoiceCommandUpdateOperationSpec.m new file mode 100644 index 000000000..ca8c9cfbd --- /dev/null +++ b/SmartDeviceLinkTests/DevAPISpecs/SDLVoiceCommandUpdateOperationSpec.m @@ -0,0 +1,249 @@ +#import <Quick/Quick.h> +#import <Nimble/Nimble.h> + +#import "SDLAddCommand.h" +#import "SDLAddCommandResponse.h" +#import "SDLDeleteCommand.h" +#import "SDLDeleteCommandResponse.h" +#import "SDLError.h" +#import "SDLFileManager.h" +#import "SDLHMILevel.h" +#import "SDLVoiceCommand.h" +#import "SDLVoiceCommandManager.h" +#import "SDLVoiceCommandUpdateOperation.h" +#import "TestConnectionManager.h" + +@interface SDLVoiceCommand() + +@property (assign, nonatomic) UInt32 commandId; + +@end + +QuickSpecBegin(SDLVoiceCommandUpdateOperationSpec) + +__block SDLDeleteCommandResponse *successDelete = nil; +__block SDLDeleteCommandResponse *failDelete = nil; +__block SDLAddCommandResponse *successAdd = nil; +__block SDLAddCommandResponse *failAdd = nil; + +__block SDLVoiceCommand *newVoiceCommand1 = [[SDLVoiceCommand alloc] initWithVoiceCommands:@[@"NewVC1"] handler:^{}]; +__block SDLVoiceCommand *newVoiceCommand2 = [[SDLVoiceCommand alloc] initWithVoiceCommands:@[@"NewVC2"] handler:^{}]; +__block SDLVoiceCommand *oldVoiceCommand1 = [[SDLVoiceCommand alloc] initWithVoiceCommands:@[@"OldVC1"] handler:^{}]; +__block SDLVoiceCommand *oldVoiceCommand2 = [[SDLVoiceCommand alloc] initWithVoiceCommands:@[@"OldVC2"] handler:^{}]; + + +describe(@"a voice command operation", ^{ + __block TestConnectionManager *testConnectionManager = nil; + __block SDLVoiceCommandUpdateOperation *testOp = nil; + + __block NSError *callbackError = nil; + __block NSArray<SDLVoiceCommand *> *callbackCurrentVoiceCommands = nil; + + beforeEach(^{ + successDelete = [[SDLDeleteCommandResponse alloc] init]; + successDelete.success = @YES; + successDelete.resultCode = SDLResultSuccess; + + failDelete = [[SDLDeleteCommandResponse alloc] init]; + failDelete.success = @NO; + failDelete.resultCode = SDLResultDisallowed; + + successAdd = [[SDLAddCommandResponse alloc] init]; + successAdd.success = @YES; + successAdd.resultCode = SDLResultSuccess; + + failAdd = [[SDLAddCommandResponse alloc] init]; + failAdd.success = @NO; + failAdd.resultCode = SDLResultDisallowed; + + newVoiceCommand1.commandId = 1; + newVoiceCommand2.commandId = 2; + oldVoiceCommand1.commandId = 3; + oldVoiceCommand2.commandId = 4; + + testConnectionManager = [[TestConnectionManager alloc] init]; + testOp = nil; + + callbackError = nil; + callbackCurrentVoiceCommands = nil; + }); + + // should have a priority of 'normal' + it(@"should have a priority of 'normal'", ^{ + testOp = [[SDLVoiceCommandUpdateOperation alloc] initWithConnectionManager:testConnectionManager pendingVoiceCommands:@[] oldVoiceCommands:@[] updateCompletionHandler:^(NSArray<SDLVoiceCommand *> * _Nonnull newCurrentVoiceCommands, NSError * _Nullable error) { + }]; + + expect(@(testOp.queuePriority)).to(equal(@(NSOperationQueuePriorityNormal))); + }); + + // initializing the operation + describe(@"initializing the operation", ^{ + testOp = [[SDLVoiceCommandUpdateOperation alloc] initWithConnectionManager:testConnectionManager pendingVoiceCommands:@[newVoiceCommand1, newVoiceCommand2] oldVoiceCommands:@[oldVoiceCommand1, oldVoiceCommand2] updateCompletionHandler:^(NSArray<SDLVoiceCommand *> * _Nonnull newCurrentVoiceCommands, NSError * _Nullable error) {}]; + + expect(testOp.oldVoiceCommands).to(equal(@[oldVoiceCommand1, oldVoiceCommand2])); + }); + + // starting the operation + describe(@"starting the operation", ^{ + + // if it starts cancelled + context(@"if it starts cancelled", ^{ + beforeEach(^{ + testOp = [[SDLVoiceCommandUpdateOperation alloc] initWithConnectionManager:testConnectionManager pendingVoiceCommands:@[newVoiceCommand1, newVoiceCommand2] oldVoiceCommands:@[oldVoiceCommand1, oldVoiceCommand2] updateCompletionHandler:^(NSArray<SDLVoiceCommand *> * _Nonnull newCurrentVoiceCommands, NSError * _Nullable error) { + callbackCurrentVoiceCommands = newCurrentVoiceCommands; + callbackError = error; + }]; + [testOp cancel]; + [testOp start]; + }); + + it(@"should return immediately with an error", ^{ + expect(callbackError).toEventuallyNot(beNil()); + }); + }); + + // if it has voice commands to delete + context(@"if it has voice commands to delete", ^{ + beforeEach(^{ + testOp = [[SDLVoiceCommandUpdateOperation alloc] initWithConnectionManager:testConnectionManager pendingVoiceCommands:@[] oldVoiceCommands:@[oldVoiceCommand1, oldVoiceCommand2] updateCompletionHandler:^(NSArray<SDLVoiceCommand *> * _Nonnull newCurrentVoiceCommands, NSError * _Nullable error) { + callbackCurrentVoiceCommands = newCurrentVoiceCommands; + callbackError = error; + }]; + [testOp start]; + }); + + // and the delete succeeds + context(@"and the delete succeeds", ^{ + beforeEach(^{ + SDLDeleteCommandResponse *deleteOld1 = [[SDLDeleteCommandResponse alloc] init]; + deleteOld1.success = @YES; + deleteOld1.resultCode = SDLResultSuccess; + + SDLDeleteCommandResponse *deleteOld2 = [[SDLDeleteCommandResponse alloc] init]; + deleteOld2.success = @YES; + deleteOld2.resultCode = SDLResultSuccess; + + [testConnectionManager respondToRequestWithResponse:deleteOld1 requestNumber:0 error:nil]; + [testConnectionManager respondToRequestWithResponse:deleteOld2 requestNumber:1 error:nil]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + }); + + it(@"should update the current voice commands", ^{ + expect(callbackCurrentVoiceCommands).to(haveCount(0)); + expect(callbackError).to(beNil()); + expect(testConnectionManager.receivedRequests).to(haveCount(2)); + }); + }); + + // and the delete fails + context(@"and the delete fails", ^{ + beforeEach(^{ + [testConnectionManager respondToRequestWithResponse:failDelete requestNumber:0 error:[NSError sdl_lifecycle_failedWithBadResult:SDLResultDisallowed info:nil]]; + [testConnectionManager respondToRequestWithResponse:failDelete requestNumber:1 error:[NSError sdl_lifecycle_failedWithBadResult:SDLResultDisallowed info:nil]]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:NO]; + }); + + it(@"should update the current voice commands and attempt to send the adds", ^{ + expect(callbackCurrentVoiceCommands).to(haveCount(2)); + expect(callbackError).toNot(beNil()); + expect(testConnectionManager.receivedRequests).to(haveCount(2)); + }); + }); + + // and the delete partially fails + context(@"and the delete partially fails", ^{ + beforeEach(^{ + [testConnectionManager respondToRequestWithResponse:failDelete requestNumber:0 error:[NSError sdl_lifecycle_failedWithBadResult:SDLResultDisallowed info:nil]]; + [testConnectionManager respondToRequestWithResponse:successDelete requestNumber:1 error:nil]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:NO]; + }); + + it(@"should update the current voice commands and attempt to send the adds", ^{ + expect(callbackCurrentVoiceCommands).to(haveCount(1)); + expect(callbackError).toNot(beNil()); + expect(testConnectionManager.receivedRequests).to(haveCount(2)); + }); + }); + }); + + context(@"if it doesn't have any voice commands to delete", ^{ + beforeEach(^{ + testOp = [[SDLVoiceCommandUpdateOperation alloc] initWithConnectionManager:testConnectionManager pendingVoiceCommands:@[newVoiceCommand1, newVoiceCommand2] oldVoiceCommands:@[] updateCompletionHandler:^(NSArray<SDLVoiceCommand *> * _Nonnull newCurrentVoiceCommands, NSError * _Nullable error) { + callbackCurrentVoiceCommands = newCurrentVoiceCommands; + callbackError = error; + }]; + [testOp start]; + }); + + context(@"adding voice commands", ^{ + context(@"and the add succeeds", ^{ + beforeEach(^{ + SDLAddCommandResponse *addNew1 = [[SDLAddCommandResponse alloc] init]; + addNew1.success = @YES; + addNew1.resultCode = SDLResultSuccess; + + SDLAddCommandResponse *addNew2 = [[SDLAddCommandResponse alloc] init]; + addNew2.success = @YES; + addNew2.resultCode = SDLResultSuccess; + + [testConnectionManager respondToRequestWithResponse:addNew1 requestNumber:0 error:nil]; + [testConnectionManager respondToRequestWithResponse:addNew2 requestNumber:1 error:nil]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:YES]; + }); + + it(@"should update the current voice commands", ^{ + expect(callbackCurrentVoiceCommands).to(haveCount(2)); + expect(callbackError).to(beNil()); + expect(testConnectionManager.receivedRequests).to(haveCount(2)); + }); + }); + + context(@"and the add fails", ^{ + beforeEach(^{ + SDLAddCommandResponse *addNew1 = [[SDLAddCommandResponse alloc] init]; + addNew1.success = @NO; + addNew1.resultCode = SDLResultDisallowed; + + SDLAddCommandResponse *addNew2 = [[SDLAddCommandResponse alloc] init]; + addNew2.success = @NO; + addNew2.resultCode = SDLResultDisallowed; + + [testConnectionManager respondToRequestWithResponse:addNew1 requestNumber:0 error:[NSError sdl_lifecycle_failedWithBadResult:SDLResultDisallowed info:nil]]; + [testConnectionManager respondToRequestWithResponse:addNew2 requestNumber:1 error:[NSError sdl_lifecycle_failedWithBadResult:SDLResultDisallowed info:nil]]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:NO]; + }); + + it(@"should update the current voice commands", ^{ + expect(callbackCurrentVoiceCommands).to(haveCount(0)); + expect(callbackError).toNot(beNil()); + expect(testConnectionManager.receivedRequests).to(haveCount(2)); + }); + }); + + context(@"and the add partially fails", ^{ + beforeEach(^{ + SDLAddCommandResponse *addNew1 = [[SDLAddCommandResponse alloc] init]; + addNew1.success = @NO; + addNew1.resultCode = SDLResultDisallowed; + + SDLAddCommandResponse *addNew2 = [[SDLAddCommandResponse alloc] init]; + addNew2.success = @YES; + addNew2.resultCode = SDLResultSuccess; + + [testConnectionManager respondToRequestWithResponse:addNew1 requestNumber:0 error:[NSError sdl_lifecycle_failedWithBadResult:SDLResultDisallowed info:nil]]; + [testConnectionManager respondToRequestWithResponse:addNew2 requestNumber:1 error:nil]; + [testConnectionManager respondToLastMultipleRequestsWithSuccess:NO]; + }); + + it(@"should update the current voice commands", ^{ + expect(callbackCurrentVoiceCommands).to(haveCount(1)); + expect(callbackError).toNot(beNil()); + expect(testConnectionManager.receivedRequests).to(haveCount(2)); + }); + }); + }); + }); + }); +}); + +QuickSpecEnd diff --git a/SmartDeviceLinkTests/TestConnectionRequestObject.h b/SmartDeviceLinkTests/TestConnectionRequestObject.h new file mode 100644 index 000000000..4bc6b5098 --- /dev/null +++ b/SmartDeviceLinkTests/TestConnectionRequestObject.h @@ -0,0 +1,23 @@ +// +// TestConnectionRequestObject.h +// SmartDeviceLinkTests +// +// Created by Joel Fischer on 11/13/20. +// Copyright © 2020 smartdevicelink. All rights reserved. +// + +#import <Foundation/Foundation.h> +#import <SmartDeviceLink/SmartDeviceLink.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface TestConnectionRequestObject : NSObject + +@property (strong, nonatomic) __kindof SDLRPCMessage *message; +@property (copy, nonatomic, nullable) SDLResponseHandler responseHandler; + +- (instancetype)initWithMessage:(__kindof SDLRPCMessage *)message responseHandler:(nullable SDLResponseHandler)handler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLinkTests/TestConnectionRequestObject.m b/SmartDeviceLinkTests/TestConnectionRequestObject.m new file mode 100644 index 000000000..a1aa57bc4 --- /dev/null +++ b/SmartDeviceLinkTests/TestConnectionRequestObject.m @@ -0,0 +1,23 @@ +// +// TestConnectionRequestObject.m +// SmartDeviceLinkTests +// +// Created by Joel Fischer on 11/13/20. +// Copyright © 2020 smartdevicelink. All rights reserved. +// + +#import "TestConnectionRequestObject.h" + +@implementation TestConnectionRequestObject + +- (instancetype)initWithMessage:(__kindof SDLRPCMessage *)message responseHandler:(SDLResponseHandler)handler { + self = [super init]; + if (!self) { return nil; } + + _message = message; + _responseHandler = handler; + + return self; +} + +@end diff --git a/SmartDeviceLinkTests/TestUtilities/TestConnectionManager.h b/SmartDeviceLinkTests/TestUtilities/TestConnectionManager.h index 5b07db771..d3dd50a19 100644 --- a/SmartDeviceLinkTests/TestUtilities/TestConnectionManager.h +++ b/SmartDeviceLinkTests/TestUtilities/TestConnectionManager.h @@ -9,6 +9,7 @@ #import <Foundation/Foundation.h> #import "SDLConnectionManagerType.h" #import "SDLNotificationConstants.h" +#import "TestConnectionRequestObject.h" NS_ASSUME_NONNULL_BEGIN @@ -20,12 +21,9 @@ NS_ASSUME_NONNULL_BEGIN /** * All received requests. Chronological order. The 0th element will be the first request received; the nth request will be the n+1th request received. */ -@property (copy, nonatomic, readonly) NSMutableArray<__kindof SDLRPCMessage *> *receivedRequests; +@property (copy, nonatomic, readonly) NSMutableArray<__kindof TestConnectionRequestObject *> *receivedRequestObjects; -/** - * The block passed for the last request send with sendRequest:withCompletionHandler: - */ -@property (copy, nonatomic, nullable) SDLResponseHandler lastRequestBlock; +@property (copy, nonatomic, readonly) NSArray<__kindof SDLRPCMessage *> *receivedRequests; @property (copy, nonatomic, nullable) NSMutableArray<SDLMultipleRequestCompletionHandler> *multipleCompletionBlocks; diff --git a/SmartDeviceLinkTests/TestUtilities/TestConnectionManager.m b/SmartDeviceLinkTests/TestUtilities/TestConnectionManager.m index ded122d0a..4774f8d09 100644 --- a/SmartDeviceLinkTests/TestUtilities/TestConnectionManager.m +++ b/SmartDeviceLinkTests/TestUtilities/TestConnectionManager.m @@ -21,21 +21,28 @@ NS_ASSUME_NONNULL_BEGIN return nil; } - _receivedRequests = [NSMutableArray<__kindof SDLRPCMessage *> array]; + _receivedRequestObjects = [NSMutableArray<TestConnectionRequestObject *> array]; _multipleCompletionBlocks = [NSMutableArray array]; return self; } +- (NSArray<__kindof SDLRPCMessage *> *)receivedRequests { + NSMutableArray<__kindof SDLRPCMessage *> *requests = [NSMutableArray array]; + [_receivedRequestObjects enumerateObjectsUsingBlock:^(__kindof TestConnectionRequestObject * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + [requests addObject:obj.message]; + }]; + + return [requests copy]; +} + - (void)sendConnectionRPC:(__kindof SDLRPCMessage *)rpc { - [self.receivedRequests addObject:rpc]; + [self.receivedRequestObjects addObject:[[TestConnectionRequestObject alloc] initWithMessage:rpc responseHandler:nil]]; } - (void)sendConnectionRequest:(__kindof SDLRPCRequest *)request withResponseHandler:(nullable SDLResponseHandler)handler { - self.lastRequestBlock = handler; - SDLRPCRequest *requestRPC = (SDLRPCRequest *)request; - requestRPC.correlationID = [self test_nextCorrelationID]; - [self.receivedRequests addObject:requestRPC]; + request.correlationID = [self test_nextCorrelationID]; + [self.receivedRequestObjects addObject:[[TestConnectionRequestObject alloc] initWithMessage:request responseHandler:handler]]; } - (void)sendConnectionManagerRequest:(__kindof SDLRPCRequest *)request withResponseHandler:(nullable SDLResponseHandler)handler { @@ -48,11 +55,11 @@ NS_ASSUME_NONNULL_BEGIN - (void)sendRequests:(nonnull NSArray<SDLRPCRequest *> *)requests progressHandler:(nullable SDLMultipleAsyncRequestProgressHandler)progressHandler completionHandler:(nullable SDLMultipleRequestCompletionHandler)completionHandler { [requests enumerateObjectsUsingBlock:^(SDLRPCRequest * _Nonnull request, NSUInteger idx, BOOL * _Nonnull stop) { - [self sendConnectionRequest:request withResponseHandler:nil]; - - if (progressHandler != nil) { - progressHandler(request, nil, nil, (double)idx / (double)requests.count); - } + [self sendConnectionRequest:request withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) { + if (progressHandler != nil) { + progressHandler(request, response, error, (double)idx / (double)requests.count); + } + }]; }]; [_multipleCompletionBlocks addObject:completionHandler]; @@ -79,8 +86,9 @@ NS_ASSUME_NONNULL_BEGIN thisError = error; } - if (self.lastRequestBlock != nil) { - self.lastRequestBlock(self.receivedRequests.lastObject, response, thisError); + TestConnectionRequestObject *lastObject = self.receivedRequestObjects.lastObject; + if (lastObject.responseHandler != nil) { + lastObject.responseHandler((SDLRPCRequest *)lastObject.message, response, thisError); } else { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Attempted to respond to last request, but there was no last request block" userInfo:nil]; } @@ -94,8 +102,9 @@ NS_ASSUME_NONNULL_BEGIN thisError = error; } - if (self.lastRequestBlock != nil) { - self.lastRequestBlock(self.receivedRequests[requestNumber], response, thisError); + TestConnectionRequestObject *requestObj = self.receivedRequestObjects[requestNumber]; + if (requestObj.responseHandler != nil) { + requestObj.responseHandler((SDLRPCRequest *)requestObj.message, response, thisError); } else { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Attempted to respond to last request, but there was no last request block" userInfo:nil]; } @@ -110,8 +119,8 @@ NS_ASSUME_NONNULL_BEGIN } - (void)reset { - _receivedRequests = [NSMutableArray<__kindof SDLRPCMessage *> array]; - _lastRequestBlock = nil; + _receivedRequestObjects = [NSMutableArray<__kindof TestConnectionRequestObject *> array]; + _multipleCompletionBlocks = [NSMutableArray array]; } |