diff --git a/SmartDeviceLink-iOS.xcodeproj/xcshareddata/xcschemes/SmartDeviceLink-Example-ObjC.xcscheme b/SmartDeviceLink-iOS.xcodeproj/xcshareddata/xcschemes/SmartDeviceLink-Example-ObjC.xcscheme new file mode 100644 index 000000000..4d9f2d40f --- /dev/null +++ b/SmartDeviceLink-iOS.xcodeproj/xcshareddata/xcschemes/SmartDeviceLink-Example-ObjC.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SmartDeviceLink/SDLError.h b/SmartDeviceLink/SDLError.h index 89cf49208..0351239d5 100644 --- a/SmartDeviceLink/SDLError.h +++ b/SmartDeviceLink/SDLError.h @@ -23,6 +23,7 @@ extern SDLErrorDomain *const SDLErrorDomainTextAndGraphicManager; extern SDLErrorDomain *const SDLErrorDomainSoftButtonManager; extern SDLErrorDomain *const SDLErrorDomainMenuManager; extern SDLErrorDomain *const SDLErrorDomainChoiceSetManager; +extern SDLErrorDomain *const SDLErrorDomainSystemCapabilityManager; extern SDLErrorDomain *const SDLErrorDomainTransport; extern SDLErrorDomain *const SDLErrorDomainRPCStore; @@ -75,6 +76,13 @@ extern SDLErrorDomain *const SDLErrorDomainRPCStore; + (NSError *)sdl_choiceSetManager_failedToCreateMenuItems; + (NSError *)sdl_choiceSetManager_incorrectState:(NSString *)state; + +#pragma mark System Capability Manager + ++ (NSError *)sdl_systemCapabilityManager_moduleDoesNotSupportSystemCapabilities; ++ (NSError *)sdl_systemCapabilityManager_cannotUpdateInHMINONE; ++ (NSError *)sdl_systemCapabilityManager_cannotUpdateTypeDISPLAYS; + #pragma mark Transport + (NSError *)sdl_transport_unknownError; diff --git a/SmartDeviceLink/SDLError.m b/SmartDeviceLink/SDLError.m index efe518cb8..b06d3dc3e 100644 --- a/SmartDeviceLink/SDLError.m +++ b/SmartDeviceLink/SDLError.m @@ -21,6 +21,7 @@ SDLErrorDomain *const SDLErrorDomainSoftButtonManager = @"com.sdl.softbuttonmanager.error"; SDLErrorDomain *const SDLErrorDomainMenuManager = @"com.sdl.menumanager.error"; SDLErrorDomain *const SDLErrorDomainChoiceSetManager = @"com.sdl.choicesetmanager.error"; +SDLErrorDomain *const SDLErrorDomainSystemCapabilityManager = @"com.sdl.systemcapabilitymanager.error"; SDLErrorDomain *const SDLErrorDomainTransport = @"com.sdl.transport.error"; SDLErrorDomain *const SDLErrorDomainRPCStore = @"com.sdl.rpcStore.error"; @@ -288,6 +289,35 @@ + (NSError *)sdl_choiceSetManager_incorrectState:(SDLChoiceManagerState *)state return [NSError errorWithDomain:SDLErrorDomainChoiceSetManager code:SDLChoiceSetManagerErrorInvalidState userInfo:userInfo]; } +#pragma mark System Capability Manager + ++ (NSError *)sdl_systemCapabilityManager_moduleDoesNotSupportSystemCapabilities { + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: NSLocalizedString(@"Module does not understand system capabilities", nil), + NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"The connected module does not support system capabilities", nil), + NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Use isCapabilitySupported to find out if the feature is supported on the head unit, but no more information about the feature is available on this module", nil) + }; + return [NSError errorWithDomain:SDLErrorDomainSystemCapabilityManager code:SDLSystemCapabilityManagerErrorModuleDoesNotSupportSystemCapabilities userInfo:userInfo]; +} + ++ (NSError *)sdl_systemCapabilityManager_cannotUpdateInHMINONE { + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: NSLocalizedString(@"System capabilities cannot be updated in HMI NONE.", nil), + NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"The system capability manager attempted to subscribe or update a system capability in HMI NONE, which is not allowed.", nil), + NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Wait until you are in HMI BACKGROUND, LIMITED, OR FULL before subscribing or updating a capability.", nil) + }; + return [NSError errorWithDomain:SDLErrorDomainSystemCapabilityManager code:SDLSystemCapabilityManagerErrorHMINone userInfo:userInfo]; +} + ++ (NSError *)sdl_systemCapabilityManager_cannotUpdateTypeDISPLAYS { + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: NSLocalizedString(@"System capability type DISPLAYS cannot be updated.", nil), + NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"The system capability manager attempted to update system capability type DISPLAYS, which is not allowed.", nil), + NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Subscribe to DISPLAYS to automatically receive updates or retrieve a cached display capability value directly from the SystemCapabilityManager.", nil) + }; + return [NSError errorWithDomain:SDLErrorDomainSystemCapabilityManager code:SDLSystemCapabilityManagerErrorCannotUpdateTypeDisplays userInfo:userInfo]; +} + #pragma mark Transport + (NSError *)sdl_transport_unknownError { @@ -381,7 +411,7 @@ + (NSException *)sdl_invalidLockscreenSetupException { + (NSException *)sdl_invalidSelectorExceptionWithSelector:(SEL)selector { return [NSException exceptionWithName:@"com.sdl.systemCapabilityManager.selectorException" - reason:[NSString stringWithFormat:@"Capability observation selector: %@ does not match possible selectors, which must have either 0 or 1 parameters", NSStringFromSelector(selector)] + reason:[NSString stringWithFormat:@"Capability observation selector: %@ does not match possible selectors, which must have between 0 and 3 parameters, or is not a selector on the observer object. Check that your selector is formatted correctly, and that your observer is not nil. You should unsubscribe an observer before it goes to nil.", NSStringFromSelector(selector)] userInfo:nil]; } diff --git a/SmartDeviceLink/SDLErrorConstants.h b/SmartDeviceLink/SDLErrorConstants.h index f6c7e27ab..a4e80c34f 100644 --- a/SmartDeviceLink/SDLErrorConstants.h +++ b/SmartDeviceLink/SDLErrorConstants.h @@ -152,6 +152,12 @@ typedef NS_ENUM(NSInteger, SDLChoiceSetManagerError) { SDLChoiceSetManagerErrorInvalidState = -5 }; +typedef NS_ENUM(NSInteger, SDLSystemCapabilityManagerError) { + SDLSystemCapabilityManagerErrorModuleDoesNotSupportSystemCapabilities = -1, + SDLSystemCapabilityManagerErrorHMINone = -2, + SDLSystemCapabilityManagerErrorCannotUpdateTypeDisplays = -3 +}; + /** * Errors associated with transport. */ diff --git a/SmartDeviceLink/SDLSystemCapabilityManager.h b/SmartDeviceLink/SDLSystemCapabilityManager.h index f5695b3bf..ca1c35792 100644 --- a/SmartDeviceLink/SDLSystemCapabilityManager.h +++ b/SmartDeviceLink/SDLSystemCapabilityManager.h @@ -51,14 +51,18 @@ typedef void (^SDLUpdateCapabilityHandler)(NSError * _Nullable error, SDLSystemC typedef void (^SDLCapabilityUpdateHandler)(SDLSystemCapability *capability); /** - A manager that handles updating and subscribing to SDL capabilities. + An observer block for whenever a subscription or value is retrieved. + + @param capability The capability that was updated. + @param subscribed Whether or not the capability was subscribed or if the capability will only be pulled once. + @param error An error that occurred. */ -@interface SDLSystemCapabilityManager : NSObject +typedef void (^SDLCapabilityUpdateWithErrorHandler)(SDLSystemCapability * _Nullable capability, BOOL subscribed, NSError * _Nullable error); /** - YES if subscriptions are available on the connected head unit. If NO, calls to `subscribeToCapabilityType:withBlock` and `subscribeToCapabilityType:withObserver:selector` will fail. + A manager that handles updating and subscribing to SDL capabilities. */ -@property (assign, nonatomic, readonly) BOOL supportsSubscriptions; +@interface SDLSystemCapabilityManager : NSObject /** * Provides window capabilities of all displays connected with SDL. By default, one display is connected and supported which includes window capability information of the default main window of the display. May be nil if the system has not provided display and window capability information yet. @@ -212,6 +216,11 @@ typedef void (^SDLCapabilityUpdateHandler)(SDLSystemCapability *capability); */ @property (nullable, strong, nonatomic, readonly) SDLWindowCapability *defaultMainWindowCapability; +/** + YES if subscriptions are available on the connected module and you will automatically be notified if the value changes on the module. If NO, calls to `subscribe` methods will subscribe to updates, but the module will not automatically notify you. You will need to call `updateWithCapabilityType:completionHandler:` to force an update if you need one (though this should be rare). This does not apply to the `DISPLAYS` capability type which you can always subscribe to. + */ +@property (assign, nonatomic, readonly) BOOL supportsSubscriptions; + /** Init is unavailable. Dependencies must be injected using initWithConnectionManager: @@ -239,53 +248,87 @@ typedef void (^SDLCapabilityUpdateHandler)(SDLSystemCapability *capability); - (void)stop; /** - * Retrieves a capability type from the remote system. This function must be called in order to retrieve the values for `navigationCapability`, `phoneCapability`, `videoStreamingCapability`, `remoteControlCapability`, and `appServicesCapabilities`. If you do not call this method first, those values will be nil. After calling this method, assuming there is no error in the handler, you may retrieve the capability you requested from the manager within the handler. + * Returns the window capability of one of your app's windows with the specified window ID that is on the primary display (i.e. the head unit itself). This is a convenience method to easily access capabilities of windows such as your apps' widget windows. + * + * To get the capabilities of the main window on the main display (i.e. your app's primary app screen on the head unit itself). * - * @param type The type of capability to retrieve - * @param handler The handler to be called when the retrieval is complete + * @param windowID The ID of the window from which to get capabilities + * @returns The window window capabilities of the window with the specified windowID, or nil if the window is not known or no window capabilities exist. */ -- (void)updateCapabilityType:(SDLSystemCapabilityType)type completionHandler:(SDLUpdateCapabilityHandler)handler; - -/** - Subscribe to a particular capability type using a block callback +- (nullable SDLWindowCapability *)windowCapabilityWithWindowID:(NSUInteger)windowID; - @param type The type of capability to subscribe to - @param block The block to be called when the capability is updated - @return An object that can be used to unsubscribe the block using unsubscribeFromCapabilityType:withObserver: by passing it in the observer callback, or nil if subscriptions aren't available on this head unit - */ -- (nullable id)subscribeToCapabilityType:(SDLSystemCapabilityType)type withBlock:(SDLCapabilityUpdateHandler)block; +/// Returns whether or not the capability type is supported on the module. You can use this to check if subscribing to the capability will work. If this returns NO, then the feature is not supported by the head unit. If YES, the feature is supported by the head unit. You can subscribe to the capability type to get more information about the capability's support and features on the connected module. +/// @param type The SystemCapabilityType that will be checked. +/// @return Whether or not `type` is supported by the connected head unit. +- (BOOL)isCapabilitySupported:(SDLSystemCapabilityType)type NS_SWIFT_NAME(isCapabilitySupported(type:)); /** - * Subscribe to a particular capability type with a selector callback. The selector supports the following parameters: - * - * 1. No parameters e.g. `- (void)phoneCapabilityUpdated;` - * 2. One `SDLSystemCapability *` parameter e.g. `- (void)phoneCapabilityUpdated:(SDLSystemCapability *)capability` - * - * This method will be called immediately with the current value and called every time the value is updated on RPC v5.1.0+ systems (`supportsSubscriptions == YES`). If this method is called on a sub-v5.1.0 system (`supportsSubscriptions == NO`), the method will return `NO` and the selector will never be called. + * This method has been superseded by the `subscribeToCapabilityType:` methods. You should use one of those methods instead unless you only want a value once (you don't want to keep a long-lasting observer) and it must be current (most capabilities do not need to be updated). If you have a separate subscription observer and are connected to a head unit that does not support subscriptions, when this method returns, it will also call all subscription callbacks that you've set up with the new value if there is one. Therefore, you can use this method to force an update to all subscriptions of that particular type on head units that don't support subscriptions (`supportsSubscriptions == NO`). * - * @param type The type of the system capability to subscribe to - * @param observer The object that will have `selector` called whenever the capability is updated - * @param selector The selector on `observer` that will be called whenever the capability is updated - * @return Whether or not the subscription succeeded. `NO` if the connected system doesn't support capability subscriptions, or if the `selector` doesn't support the correct parameters (see above) + * @param type The type of capability to retrieve + * @param handler The handler to be called when the retrieval is complete */ +- (void)updateCapabilityType:(SDLSystemCapabilityType)type completionHandler:(SDLUpdateCapabilityHandler)handler; + +/// Subscribe to a particular capability type using a block callback. +/// +/// On v5.1.0+ systems (where `supportsSubscriptions == YES`): +/// This method will be called immediately with the current value if a subscription already exists and will be called every time the value is updated. +/// +/// On sub-v5.1.0 systems (where `supportsSubscriptions == NO`): +/// The method will be called immediately with the current value and will _not_ be automatically called every time the value is updated, unless the `type` is `DISPLAYS` which is supported on every version. If `updateCapabilityType:completionHandler` is called and a new value is retrieved, this value will be updated then. If this is the first subscription of this `SDLSystemCapabilityType`, then the value will be retrieved and returned. +/// +/// @param type The type of capability to subscribe to +/// @param block The block to be called when the capability is updated +/// @return An object that can be used to unsubscribe the block using unsubscribeFromCapabilityType:withObserver: by passing it in the observer callback, or nil if the manager can't attempt the subscription for some reason (such as the app being in HMI_NONE and the type is not DISPLAYS). +- (nullable id)subscribeToCapabilityType:(SDLSystemCapabilityType)type withBlock:(SDLCapabilityUpdateHandler)block __deprecated_msg("use subscribeToCapabilityType:withUpdateHandler: instead"); + +/// Subscribe to a particular capability type using a handler callback. +/// +/// On v5.1.0+ systems (where `supportsSubscriptions == YES`): +/// This method will be called immediately with the current value if a subscription already exists and will be called every time the value is updated. +/// +/// Note that when the cached value is returned, the `subscribed` flag on the handler will be false until the subscription completes successfully and a new value is retrieved. +/// +/// On sub-v5.1.0 systems (where `supportsSubscriptions == NO`): +/// The method will be called immediately with the current value and will _not_ be automatically called every time the value is updated, unless the `type` is `DISPLAYS` which is supported on every version. If `updateCapabilityType:completionHandler` is called and a new value is retrieved, this value will be updated then. If this is the first subscription of this `SDLSystemCapabilityType`, then the value will be retrieved and returned. +/// +/// @param type The type of capability to subscribe to +/// @param handler The block to be called when the capability is updated with an error if one occurs +/// @return An object that can be used to unsubscribe the block using unsubscribeFromCapabilityType:withObserver: by passing it in the observer callback, or nil if the manager can't attempt the subscription for some reason (such as the app being in HMI_NONE and the type is not DISPLAYS). +- (nullable id)subscribeToCapabilityType:(SDLSystemCapabilityType)type withUpdateHandler:(SDLCapabilityUpdateWithErrorHandler)handler NS_SWIFT_NAME(subscribe(capabilityType:updateHandler:)); + + +/// Subscribe to a particular capability type with a selector callback. +/// +/// The selector supports the following parameters: +/// +/// 1. No parameters e.g. `- (void)phoneCapabilityUpdated;` +/// +/// 2. One `SDLSystemCapability *` parameter, e.g. `- (void)phoneCapabilityUpdated:(SDLSystemCapability *)capability` +/// +/// 3. Two parameters, one `SDLSystemCapability *` parameter, and one `NSError` parameter, e.g. `- (void)phoneCapabilityUpdated:(SDLSystemCapability *)capability error:(NSError *)error` +/// +/// 4. Three parameters, one `SDLSystemCapability *` parameter, one `NSError` parameter, and one `BOOL` parameter e.g. `- (void)phoneCapabilityUpdated:(SDLSystemCapability *)capability error:(NSError *)error subscribed:(BOOL)subscribed` +/// +/// On v5.1.0+ systems (where `supportsSubscriptions == YES`): +/// This method will be called immediately with the current value if a subscription already exists and will be called every time the value is updated. +/// +/// On sub-v5.1.0 systems (where `supportsSubscriptions == NO`): +/// The method will be called immediately with the current value and will _not_ be automatically called every time the value is updated, unless the `type` is `DISPLAYS` which is supported on every version. If `updateCapabilityType:completionHandler` is called and a new value is retrieved, this value will be updated then. If this is the first subscription of this `SDLSystemCapabilityType`, then the value will be retrieved and returned. +/// +/// @param type The type of the system capability to subscribe to +/// @param observer The object that will have `selector` called whenever the capability is updated +/// @param selector The selector on `observer` that will be called whenever the capability is updated +/// @return YES if the manager is attempting the subscription, or NO if the manager can't attempt the subscription for some reason (such as the app being in HMI_NONE and the type is not DISPLAYS), or the selector doesn't contain the correct number of parameters. - (BOOL)subscribeToCapabilityType:(SDLSystemCapabilityType)type withObserver:(id)observer selector:(SEL)selector; -/** - * Unsubscribe from a particular capability type. If it was subscribed with a block, the return value should be passed to the `observer` to unsubscribe the block. If it was subscribed with a selector, the `observer` object should be passed to unsubscribe the object selector. - * - * @param type The type of the system capability to unsubscribe from - * @param observer The object that will be unsubscribed. If a block was subscribed, the return value should be passed. If a selector was subscribed, the observer object should be passed. - */ +/// Unsubscribe from a particular capability type. If it was subscribed with a block / handler, the return value should be passed to the `observer` to unsubscribe the block. If it was subscribed with a selector, the `observer` object (on which the selector exists and is called) should be passed to unsubscribe the object selector. +/// +/// @param type The type of the system capability to unsubscribe from +/// @param observer The object that will be unsubscribed. If a block was subscribed, the return value should be passed. If a selector was subscribed, the observer object should be passed. - (void)unsubscribeFromCapabilityType:(SDLSystemCapabilityType)type withObserver:(id)observer; -/** - * Returns the window capability object of the primary display with the specified window ID. This is a convenient method to easily access capabilities of windows for instance widget windows of the main display. - * - * @param windowID The ID of the window to get capabilities - * @returns The window capability object representing the window capabilities of the window with the specified window ID or nil if the window is not known or no window capabilities exist. - */ -- (nullable SDLWindowCapability *)windowCapabilityWithWindowID:(NSUInteger)windowID; - @end NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLink/SDLSystemCapabilityManager.m b/SmartDeviceLink/SDLSystemCapabilityManager.m index 0f1c4faf3..d248da7ec 100644 --- a/SmartDeviceLink/SDLSystemCapabilityManager.m +++ b/SmartDeviceLink/SDLSystemCapabilityManager.m @@ -19,6 +19,7 @@ #import "SDLGetSystemCapability.h" #import "SDLGetSystemCapabilityResponse.h" #import "SDLGlobals.h" +#import "SDLHMICapabilities.h" #import "SDLLogMacros.h" #import "SDLNavigationCapability.h" #import "SDLNotificationConstants.h" @@ -69,12 +70,10 @@ @interface SDLSystemCapabilityManager () @property (assign, nonatomic, readwrite) BOOL supportsSubscriptions; @property (strong, nonatomic) NSMutableDictionary *> *capabilityObservers; - -@property (nullable, strong, nonatomic) SDLSystemCapability *lastReceivedCapability; - -@property (assign, nonatomic) BOOL isFirstHMILevelFull; +@property (strong, nonatomic) NSMutableDictionary *> *subscriptionStatus; @property (assign, nonatomic) BOOL shouldConvertDeprecatedDisplayCapabilities; +@property (strong, nonatomic) SDLHMILevel currentHMILevel; @end @@ -89,27 +88,20 @@ - (instancetype)initWithConnectionManager:(id)manager } _connectionManager = manager; - _isFirstHMILevelFull = NO; _shouldConvertDeprecatedDisplayCapabilities = YES; _appServicesCapabilitiesDictionary = [NSMutableDictionary dictionary]; _capabilityObservers = [NSMutableDictionary dictionary]; - for (SDLSystemCapabilityType capabilityType in [self.class sdl_systemCapabilityTypes]) { - _capabilityObservers[capabilityType] = [NSMutableArray array]; - } + _subscriptionStatus = [NSMutableDictionary dictionary]; + + _currentHMILevel = SDLHMILevelNone; [self sdl_registerForNotifications]; return self; } -- (void)start { - SDLVersion *onSystemCapabilityNotificationRPCVersion = [SDLVersion versionWithString:@"5.1.0"]; - SDLVersion *headUnitRPCVersion = SDLGlobals.sharedGlobals.rpcVersion; - if ([headUnitRPCVersion isGreaterThanOrEqualToVersion:onSystemCapabilityNotificationRPCVersion]) { - _supportsSubscriptions = YES; - } -} +- (void)start { } /** * Resets the capabilities when a transport session is closed. @@ -136,136 +128,26 @@ - (void)stop { _appServicesCapabilitiesDictionary = [NSMutableDictionary dictionary]; _supportsSubscriptions = NO; - for (SDLSystemCapabilityType capabilityType in [self.class sdl_systemCapabilityTypes]) { - _capabilityObservers[capabilityType] = [NSMutableArray array]; - } + [_capabilityObservers removeAllObjects]; + [_subscriptionStatus removeAllObjects]; + + _currentHMILevel = SDLHMILevelNone; - _isFirstHMILevelFull = NO; _shouldConvertDeprecatedDisplayCapabilities = YES; } #pragma mark - Getters +- (BOOL)supportsSubscriptions { + return [[SDLGlobals sharedGlobals].rpcVersion isGreaterThanOrEqualToVersion:[SDLVersion versionWithString:@"5.1.0"]]; +} + - (nullable SDLAppServicesCapabilities *)appServicesCapabilities { if (self.appServicesCapabilitiesDictionary.count == 0) { return nil; } return [[SDLAppServicesCapabilities alloc] initWithAppServices:self.appServicesCapabilitiesDictionary.allValues]; } -#pragma mark - Notifications - -/** - * Registers for notifications and responses from Core - */ -- (void)sdl_registerForNotifications { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sdl_registerResponse:) name:SDLDidReceiveRegisterAppInterfaceResponse object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sdl_displayLayoutResponse:) name:SDLDidReceiveSetDisplayLayoutResponse object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sdl_systemCapabilityUpdatedNotification:) name:SDLDidReceiveSystemCapabilityUpdatedNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sdl_systemCapabilityResponseNotification:) name:SDLDidReceiveGetSystemCapabilitiesResponse object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sdl_hmiStatusNotification:) name:SDLDidChangeHMIStatusNotification object:nil]; -} - -/** - * Called when a `RegisterAppInterfaceResponse` response is received from Core. The head unit capabilities are saved. - * - * @param notification The `RegisterAppInterfaceResponse` response received from Core - */ -- (void)sdl_registerResponse:(SDLRPCResponseNotification *)notification { - SDLRegisterAppInterfaceResponse *response = (SDLRegisterAppInterfaceResponse *)notification.response; - if (!response.success.boolValue) { return; } - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated" - self.displayCapabilities = response.displayCapabilities; - self.softButtonCapabilities = response.softButtonCapabilities; - self.buttonCapabilities = response.buttonCapabilities; - self.presetBankCapabilities = response.presetBankCapabilities; -#pragma clang diagnostic pop - - self.hmiCapabilities = response.hmiCapabilities; - self.hmiZoneCapabilities = response.hmiZoneCapabilities; - self.speechCapabilities = response.speechCapabilities; - self.prerecordedSpeechCapabilities = response.prerecordedSpeech; - self.vrCapability = (response.vrCapabilities.count > 0 && [response.vrCapabilities.firstObject isEqualToEnum:SDLVRCapabilitiesText]) ? YES : NO; - self.audioPassThruCapabilities = response.audioPassThruCapabilities; - self.pcmStreamCapability = response.pcmStreamCapabilities; - - self.shouldConvertDeprecatedDisplayCapabilities = YES; - self.displays = [self sdl_createDisplayCapabilityListFromRegisterResponse:response]; - - // call the observers in case the new display capability list is created from deprecated types - SDLSystemCapability *systemCapability = [[SDLSystemCapability alloc] initWithDisplayCapabilities:self.displays]; - [self sdl_callObserversForCapabilityUpdate:systemCapability handler:nil]; -} - -/** - * Called when a `SetDisplayLayoutResponse` response is received from Core. If the template was set successfully, the the new capabilities for the template are saved. - * - * @param notification The `SetDisplayLayoutResponse` response received from Core - */ -- (void)sdl_displayLayoutResponse:(SDLRPCResponseNotification *)notification { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated" - SDLSetDisplayLayoutResponse *response = (SDLSetDisplayLayoutResponse *)notification.response; -#pragma clang diagnostic pop - if (!response.success.boolValue) { return; } - - // If we've received a display capability update then we should not convert our deprecated display capabilities and we should just return - if (!self.shouldConvertDeprecatedDisplayCapabilities) { return; } - - self.displayCapabilities = response.displayCapabilities; - self.buttonCapabilities = response.buttonCapabilities; - self.softButtonCapabilities = response.softButtonCapabilities; - self.presetBankCapabilities = response.presetBankCapabilities; - - self.displays = [self sdl_createDisplayCapabilityListFromSetDisplayLayoutResponse:response]; - - // Call the observers in case the new display capability list is created from deprecated types - SDLSystemCapability *systemCapability = [[SDLSystemCapability alloc] initWithDisplayCapabilities:self.displays]; - [self sdl_callObserversForCapabilityUpdate:systemCapability handler:nil]; -} - - -/** - * Called when an `OnSystemCapabilityUpdated` notification is received from Core. The updated system capabilty is saved. - * - * @param notification The `OnSystemCapabilityUpdated` notification received from Core - */ -- (void)sdl_systemCapabilityUpdatedNotification:(SDLRPCNotificationNotification *)notification { - SDLOnSystemCapabilityUpdated *systemCapabilityUpdatedNotification = (SDLOnSystemCapabilityUpdated *)notification.notification; - [self sdl_saveSystemCapability:systemCapabilityUpdatedNotification.systemCapability completionHandler:nil]; -} - -/** - Called with a `GetSystemCapabilityResponse` notification is received from core. The updated system capability is saved. - - @param notification The `GetSystemCapabilityResponse` notification received from Core - */ -- (void)sdl_systemCapabilityResponseNotification:(SDLRPCResponseNotification *)notification { - SDLGetSystemCapabilityResponse *systemCapabilityResponse = (SDLGetSystemCapabilityResponse *)notification.response; - [self sdl_saveSystemCapability:systemCapabilityResponse.systemCapability completionHandler:nil]; -} - -/** - * Called when an `OnHMIStatus` notification is received from Core. The first time the `hmiLevel` is `FULL` attempt to subscribe to system capabilty updates. - * - * @param notification The `OnHMIStatus` notification received from Core - */ -- (void)sdl_hmiStatusNotification:(SDLRPCNotificationNotification *)notification { - SDLOnHMIStatus *hmiStatus = (SDLOnHMIStatus *)notification.notification; - - if (hmiStatus.windowID != nil && hmiStatus.windowID.integerValue != SDLPredefinedWindowsDefaultWindow) { - return; - } - - if (self.isFirstHMILevelFull || ![hmiStatus.hmiLevel isEqualToEnum:SDLHMILevelFull]) { - return; - } - - self.isFirstHMILevelFull = YES; - [self sdl_subscribeToSystemCapabilityUpdates]; -} - #pragma mark - Window And Display Capabilities - (nullable SDLWindowCapability *)windowCapabilityWithWindowID:(NSUInteger)windowID { @@ -308,6 +190,7 @@ - (nullable SDLWindowCapability *)defaultMainWindowCapability { /// @param buttons The old-style `SDLButtonCapabilities` object to convert /// @param softButtons The old-style `SDLSoftButtonCapabilities` to convert - (NSArray *)sdl_createDisplayCapabilityListFromDeprecatedDisplayCapabilities:(SDLDisplayCapabilities *)display buttons:(NSArray *)buttons softButtons:(NSArray *)softButtons { + SDLLogV(@"Creating display capability from deprecated display capabilities"); // Based on deprecated Display capabilities we don't know if widgets are supported. The default MAIN window is the only window we know is supported, so it's the only one we will expose. SDLWindowTypeCapabilities *windowTypeCapabilities = [[SDLWindowTypeCapabilities alloc] initWithType:SDLWindowTypeMain maximumNumberOfWindows:1]; #pragma clang diagnostic push @@ -352,94 +235,148 @@ For backward compatibility (AppLink 2.0) static image type is always presented #pragma mark Convert New to Deprecated -/// Convert from a WindowCapability (should be the main display's main window capability) to the deprecated old-style DisplayCapabilities -/// @param displayName The display name of the display to be converted -/// @param windowCapability The window capability to be converted -- (SDLDisplayCapabilities *)sdl_createDeprecatedDisplayCapabilitiesWithDisplayName:(NSString *)displayName windowCapability:(SDLWindowCapability *)windowCapability { - SDLDisplayCapabilities *convertedCapabilities = [[SDLDisplayCapabilities alloc] init]; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated" - convertedCapabilities.displayType = SDLDisplayTypeGeneric; // deprecated but it is mandatory -#pragma clang diagnostic pop - convertedCapabilities.displayName = displayName; - convertedCapabilities.textFields = [windowCapability.textFields copy]; - convertedCapabilities.imageFields = [windowCapability.imageFields copy]; - convertedCapabilities.templatesAvailable = [windowCapability.templatesAvailable copy]; - convertedCapabilities.numCustomPresetsAvailable = [windowCapability.numCustomPresetsAvailable copy]; - convertedCapabilities.mediaClockFormats = @[]; // mandatory field but allows empty array - convertedCapabilities.graphicSupported = @([windowCapability.imageTypeSupported containsObject:SDLImageTypeDynamic]); - - return convertedCapabilities; -} - /// Update the internal deprecated display capability methods with new values based on the current value of the default main window capability and the primary display - (void)sdl_updateDeprecatedDisplayCapabilities { + SDLLogV(@"Updating deprecated capabilities from default main window capabilities"); SDLWindowCapability *defaultMainWindowCapabilities = self.defaultMainWindowCapability; if (self.displays.count == 0) { return; } // Create the deprecated capabilities for backward compatibility if developers try to access them - self.displayCapabilities = [self sdl_createDeprecatedDisplayCapabilitiesWithDisplayName:self.displays.firstObject.displayName windowCapability:defaultMainWindowCapabilities]; + SDLDisplayCapabilities *convertedCapabilities = [[SDLDisplayCapabilities alloc] init]; + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated" + convertedCapabilities.displayType = SDLDisplayTypeGeneric; // deprecated but it is mandatory + #pragma clang diagnostic pop + convertedCapabilities.displayName = self.displays.firstObject.displayName; + convertedCapabilities.textFields = [defaultMainWindowCapabilities.textFields copy]; + convertedCapabilities.imageFields = [defaultMainWindowCapabilities.imageFields copy]; + convertedCapabilities.templatesAvailable = [defaultMainWindowCapabilities.templatesAvailable copy]; + convertedCapabilities.numCustomPresetsAvailable = [defaultMainWindowCapabilities.numCustomPresetsAvailable copy]; + convertedCapabilities.mediaClockFormats = @[]; // mandatory field but allows empty array + convertedCapabilities.graphicSupported = @([defaultMainWindowCapabilities.imageTypeSupported containsObject:SDLImageTypeDynamic]); + + self.displayCapabilities = convertedCapabilities; self.buttonCapabilities = defaultMainWindowCapabilities.buttonCapabilities; self.softButtonCapabilities = defaultMainWindowCapabilities.softButtonCapabilities; } #pragma mark - System Capability Updates +- (BOOL)isCapabilitySupported:(SDLSystemCapabilityType)type { + if ([self sdl_cachedCapabilityForType:type] != nil) { + return YES; + } else if ([type isEqualToEnum:SDLSystemCapabilityTypePhoneCall]) { + return self.hmiCapabilities.phoneCall.boolValue; + } else if ([type isEqualToEnum:SDLSystemCapabilityTypeNavigation]) { + return self.hmiCapabilities.navigation.boolValue; + } else if ([type isEqualToEnum:SDLSystemCapabilityTypeDisplays]) { + return self.hmiCapabilities.displays.boolValue; + } else if ([type isEqualToEnum:SDLSystemCapabilityTypeRemoteControl]) { + return self.hmiCapabilities.remoteControl.boolValue; + } else if ([type isEqualToEnum:SDLSystemCapabilityTypeSeatLocation]) { + return self.hmiCapabilities.seatLocation.boolValue; + } else if ([type isEqualToEnum:SDLSystemCapabilityTypeAppServices]) { + //This is a corner case that the param was not available in 5.1.0, but the app services feature was available. We have to say it's available because we don't know. + if ([[SDLGlobals sharedGlobals].rpcVersion isEqualToVersion:[SDLVersion versionWithString:@"5.1.0"]]) { + return YES; + } + + return self.hmiCapabilities.appServices.boolValue; + } else if ([type isEqualToEnum:SDLSystemCapabilityTypeVideoStreaming]) { + if ([[SDLGlobals sharedGlobals].rpcVersion isGreaterThanOrEqualToVersion:[SDLVersion versionWithString:@"3.0.0"]] && [[SDLGlobals sharedGlobals].rpcVersion isLessThanOrEqualToVersion:[SDLVersion versionWithString:@"4.4.0"]]) { + // This was before the system capability feature was added so check if graphics are supported instead using the deprecated display capabilities + return self.displayCapabilities.graphicSupported.boolValue; + } + + return self.hmiCapabilities.videoStreaming.boolValue; + } else { + return NO; + } + + + return NO; +} + +- (nullable SDLSystemCapability *)sdl_cachedCapabilityForType:(SDLSystemCapabilityType)type { + if ([type isEqualToEnum:SDLSystemCapabilityTypePhoneCall] && self.phoneCapability != nil) { + return [[SDLSystemCapability alloc] initWithPhoneCapability:self.phoneCapability]; + } else if ([type isEqualToEnum:SDLSystemCapabilityTypeNavigation] && self.navigationCapability != nil) { + return [[SDLSystemCapability alloc] initWithNavigationCapability:self.navigationCapability]; + } else if ([type isEqualToEnum:SDLSystemCapabilityTypeAppServices] && self.appServicesCapabilities != nil) { + return [[SDLSystemCapability alloc] initWithAppServicesCapabilities:self.appServicesCapabilities]; + } else if ([type isEqualToEnum:SDLSystemCapabilityTypeDisplays] && self.displays != nil) { + return [[SDLSystemCapability alloc] initWithDisplayCapabilities:self.displays]; + } else if ([type isEqualToEnum:SDLSystemCapabilityTypeSeatLocation] && self.seatLocationCapability != nil) { + return [[SDLSystemCapability alloc] initWithSeatLocationCapability:self.seatLocationCapability]; + } else if ([type isEqualToEnum:SDLSystemCapabilityTypeRemoteControl] && self.remoteControlCapability != nil) { + return [[SDLSystemCapability alloc] initWithRemoteControlCapability:self.remoteControlCapability]; + } else if ([type isEqualToEnum:SDLSystemCapabilityTypeVideoStreaming] && self.videoStreamingCapability != nil) { + return [[SDLSystemCapability alloc] initWithVideoStreamingCapability:self.videoStreamingCapability]; + } else { + return nil; + } +} + - (void)updateCapabilityType:(SDLSystemCapabilityType)type completionHandler:(SDLUpdateCapabilityHandler)handler { - if (self.supportsSubscriptions) { + SDLLogV(@"Updating capability type: %@", type); + if ([self.currentHMILevel isEqualToEnum:SDLHMILevelNone] && ![type isEqualToEnum:SDLSystemCapabilityTypeDisplays]) { + SDLLogE(@"Attempted to update type: %@ in HMI level NONE, which is not allowed. Please wait until you are in HMI BACKGROUND, LIMITED, or FULL before attempting to update any SystemCapabilityType DISPLAYS.", type); + return handler([NSError sdl_systemCapabilityManager_cannotUpdateInHMINONE], self); + } else if ([type isEqualToEnum:SDLSystemCapabilityTypeDisplays]) { + SDLLogE(@"Attempted to update type DISPLAYS, which is not allowed. You are always subscribed to displays, please either pull the cached data directly or subscribe for updates to DISPLAYS."); + return handler([NSError sdl_systemCapabilityManager_cannotUpdateTypeDISPLAYS], self); + } + + // If we support subscriptions and we're already subscribed + if (self.supportsSubscriptions && [self.subscriptionStatus[type] isEqualToNumber:@YES]) { // Just return the cached data because we get `onSystemCapability` callbacks handler(nil, self); } else { // Go and get the actual data - SDLGetSystemCapability *getSystemCapability = [[SDLGetSystemCapability alloc] initWithType:type]; - [self sdl_sendGetSystemCapability:getSystemCapability completionHandler:handler]; + __weak typeof(self) weakself = self; + [self sdl_sendGetSystemCapabilityWithType:type subscribe:nil completionHandler:^(SDLSystemCapability * _Nonnull capability, BOOL subscribed, NSError * _Nonnull error) { + handler(error, weakself); + }]; } } -/** - * A list of all possible system capability types. - * - * @return An array of all possible system capability types - */ -+ (NSArray *)sdl_systemCapabilityTypes { - return @[SDLSystemCapabilityTypeAppServices, SDLSystemCapabilityTypeNavigation, SDLSystemCapabilityTypePhoneCall, SDLSystemCapabilityTypeVideoStreaming, SDLSystemCapabilityTypeRemoteControl, SDLSystemCapabilityTypeDisplays, SDLSystemCapabilityTypeSeatLocation]; -} - # pragma mark Subscribing -/** - * Sends a subscribe request for all possible system capabilites. If connecting to Core versions 4.5+, the requested capability will be returned in the response. If connecting to Core versions 5.1+, the manager will received `OnSystemCapabilityUpdated` notifications when the capability updates if the subscription was successful. - */ -- (void)sdl_subscribeToSystemCapabilityUpdates { - for (SDLSystemCapabilityType type in [self.class sdl_systemCapabilityTypes]) { - SDLGetSystemCapability *getSystemCapability = [[SDLGetSystemCapability alloc] initWithType:type]; - if (self.supportsSubscriptions) { - getSystemCapability.subscribe = @YES; - } - - [self sdl_sendGetSystemCapability:getSystemCapability completionHandler:nil]; - } -} +/// Sends a GetSystemCapability and sends back the response +/// @param type The type to get +/// @param subscribe Whether to change the subscription status. YES to subscribe, NO to unsubscribe, nil to keep whatever the current state is +/// @param handler The handler to be returned +- (void)sdl_sendGetSystemCapabilityWithType:(SDLSystemCapabilityType)type subscribe:(nullable NSNumber *)subscribe completionHandler:(nullable SDLCapabilityUpdateWithErrorHandler)handler { + SDLLogV(@"Sending GetSystemCapability with type: %@, subscribe: %@", type, subscribe); + SDLGetSystemCapability *getSystemCapability = [[SDLGetSystemCapability alloc] initWithType:type]; + getSystemCapability.subscribe = subscribe; -/** - * Sends a `GetSystemCapability` to Core and handles the response by saving the returned data and notifying the subscriber. - * - * @param getSystemCapability The `GetSystemCapability` request to send - */ -- (void)sdl_sendGetSystemCapability:(SDLGetSystemCapability *)getSystemCapability completionHandler:(nullable SDLUpdateCapabilityHandler)handler { - __weak typeof(self) weakSelf = self; + __weak typeof(self) weakself = self; [self.connectionManager sendConnectionRequest:getSystemCapability withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) { - if (error != nil) { - // An error is returned if the request was unsuccessful or if a Generic Response was returned + if (![response isKindOfClass:[SDLGetSystemCapabilityResponse class]]) { + SDLLogE(@"GetSystemCapability failed, type: %@, did not return a GetSystemCapability response", type); + if (handler == nil) { return; } + handler(nil, NO, [NSError sdl_systemCapabilityManager_moduleDoesNotSupportSystemCapabilities]); + return; + } + + if (response.success.boolValue == false) { + SDLLogE(@"GetSystemCapability failed, type: %@, error: %@", type, error); if (handler == nil) { return; } - handler(error, weakSelf); + handler(nil, NO, error); return; } SDLGetSystemCapabilityResponse *getSystemCapabilityResponse = (SDLGetSystemCapabilityResponse *)response; - [weakSelf sdl_saveSystemCapability:getSystemCapabilityResponse.systemCapability completionHandler:handler]; + SDLLogD(@"GetSystemCapability response succeeded, type: %@, response: %@", type, getSystemCapabilityResponse); + + if (![weakself.subscriptionStatus[type] isEqualToNumber:subscribe] && weakself.supportsSubscriptions) { + weakself.subscriptionStatus[type] = subscribe; + } + + [weakself sdl_saveSystemCapability:getSystemCapabilityResponse.systemCapability error:error completionHandler:handler]; }]; } @@ -452,42 +389,38 @@ - (void)sdl_sendGetSystemCapability:(SDLGetSystemCapability *)getSystemCapabilit @param handler The handler to be called when the save completes @return Whether or not the save occurred. This can be `NO` if the new system capability is equivalent to the old capability. */ -- (BOOL)sdl_saveSystemCapability:(SDLSystemCapability *)systemCapability completionHandler:(nullable SDLUpdateCapabilityHandler)handler { - if ([self.lastReceivedCapability isEqual:systemCapability]) { - [self sdl_callObserversForCapabilityUpdate:systemCapability handler:handler]; - return NO; - } - self.lastReceivedCapability = systemCapability; +- (BOOL)sdl_saveSystemCapability:(nullable SDLSystemCapability *)systemCapability error:(nullable NSError *)error completionHandler:(nullable SDLCapabilityUpdateWithErrorHandler)handler { + SDLLogV(@"Saving system capability type: %@", systemCapability); SDLSystemCapabilityType systemCapabilityType = systemCapability.systemCapabilityType; if ([systemCapabilityType isEqualToEnum:SDLSystemCapabilityTypePhoneCall]) { if ([self.phoneCapability isEqual:systemCapability.phoneCapability]) { - [self sdl_callObserversForCapabilityUpdate:systemCapability handler:handler]; + [self sdl_callObserversForUpdate:systemCapability error:error handler:handler]; return NO; } self.phoneCapability = systemCapability.phoneCapability; } else if ([systemCapabilityType isEqualToEnum:SDLSystemCapabilityTypeNavigation]) { if ([self.navigationCapability isEqual:systemCapability.navigationCapability]) { - [self sdl_callObserversForCapabilityUpdate:systemCapability handler:handler]; + [self sdl_callObserversForUpdate:systemCapability error:error handler:handler]; return NO; } self.navigationCapability = systemCapability.navigationCapability; } else if ([systemCapabilityType isEqualToEnum:SDLSystemCapabilityTypeRemoteControl]) { if ([self.remoteControlCapability isEqual:systemCapability.remoteControlCapability]) { - [self sdl_callObserversForCapabilityUpdate:systemCapability handler:handler]; + [self sdl_callObserversForUpdate:systemCapability error:error handler:handler]; return NO; } self.remoteControlCapability = systemCapability.remoteControlCapability; } else if ([systemCapabilityType isEqualToEnum:SDLSystemCapabilityTypeSeatLocation]) { if ([self.seatLocationCapability isEqual:systemCapability.seatLocationCapability]) { - [self sdl_callObserversForCapabilityUpdate:systemCapability handler:handler]; + [self sdl_callObserversForUpdate:systemCapability error:error handler:handler]; return NO; } self.seatLocationCapability = systemCapability.seatLocationCapability; } else if ([systemCapabilityType isEqualToEnum:SDLSystemCapabilityTypeVideoStreaming]) { if ([self.videoStreamingCapability isEqual:systemCapability.videoStreamingCapability]) { - [self sdl_callObserversForCapabilityUpdate:systemCapability handler:handler]; + [self sdl_callObserversForUpdate:systemCapability error:error handler:handler]; return NO; } self.videoStreamingCapability = systemCapability.videoStreamingCapability; @@ -504,13 +437,14 @@ - (BOOL)sdl_saveSystemCapability:(SDLSystemCapability *)systemCapability complet SDLLogD(@"Updated system capability manager with new data: %@", systemCapability); - [self sdl_callObserversForCapabilityUpdate:systemCapability handler:handler]; + [self sdl_callObserversForUpdate:systemCapability error:error handler:handler]; return YES; } #pragma mark Merge Capability Deltas - (void)sdl_saveAppServicesCapabilitiesUpdate:(SDLAppServicesCapabilities *)newCapabilities { + SDLLogV(@"Saving app services capability update with new capabilities: %@", newCapabilities); for (SDLAppServiceCapability *capability in newCapabilities.appServices) { if (capability.updateReason == nil) { // First update, new capability @@ -528,6 +462,7 @@ - (void)sdl_saveAppServicesCapabilitiesUpdate:(SDLAppServicesCapabilities *)newC /// @param newCapabilities The new `DisplayCapability` update delta. - (void)sdl_saveDisplayCapabilityListUpdate:(NSArray *)newCapabilities { NSArray *oldCapabilities = self.displays; + SDLLogV(@"Saving display capability update with new capabilities: %@", newCapabilities); if (oldCapabilities == nil) { self.displays = newCapabilities; @@ -547,15 +482,18 @@ - (void)sdl_saveDisplayCapabilityListUpdate:(NSArray *)n SDLWindowCapability *oldWindow = copyWindowCapabilities[i]; NSUInteger newWindowID = newWindow.windowID ? newWindow.windowID.unsignedIntegerValue : SDLPredefinedWindowsDefaultWindow; NSUInteger oldWindowID = oldWindow.windowID ? oldWindow.windowID.unsignedIntegerValue : SDLPredefinedWindowsDefaultWindow; + if (newWindowID == oldWindowID) { - copyWindowCapabilities[i] = newWindow; // replace the old window caps with new ones + // Replace the old window caps with new ones + copyWindowCapabilities[i] = newWindow; oldFound = true; break; } } if (!oldFound) { - [copyWindowCapabilities addObject:newWindow]; // this is a new unknown window + // This is a new unknown window + [copyWindowCapabilities addObject:newWindow]; } } @@ -568,32 +506,112 @@ - (void)sdl_saveDisplayCapabilityListUpdate:(NSArray *)n #pragma mark - Manager Subscriptions - (nullable id)subscribeToCapabilityType:(SDLSystemCapabilityType)type withBlock:(SDLCapabilityUpdateHandler)block { - // DISPLAYS always works due to old-style SetDisplayLayoutRepsonse updates, but otherwise, subscriptions won't work - if (!self.supportsSubscriptions && ![type isEqualToEnum:SDLSystemCapabilityTypeDisplays]) { return nil; } - + SDLLogD(@"Subscribing to capability type: %@ with a handler (DEPRECATED)", type); SDLSystemCapabilityObserver *observerObject = [[SDLSystemCapabilityObserver alloc] initWithObserver:[[NSObject alloc] init] block:block]; - [self.capabilityObservers[type] addObject:observerObject]; + + if ([self.currentHMILevel isEqualToEnum:SDLHMILevelNone] && ![type isEqualToEnum:SDLSystemCapabilityTypeDisplays]) { + SDLLogE(@"Attempted to subscribe to type: %@ in HMI level NONE, which is not allowed. Please wait until you are in HMI BACKGROUND, LIMITED, or FULL before attempting to subscribe to any SystemCapabilityType other than DISPLAYS.", type); + [self sdl_invokeObserver:observerObject withCapabilityType:type capability:nil error:[NSError sdl_systemCapabilityManager_cannotUpdateInHMINONE]]; + return nil; + } + + if (self.capabilityObservers[type] == nil) { + SDLLogD(@"This is the first subscription to capability type: %@, sending a GetSystemCapability with subscribe true", type); + self.capabilityObservers[type] = [NSMutableArray arrayWithObject:observerObject]; + + // We don't want to send this for the displays type because that's automatically subscribed + if (![type isEqualToEnum:SDLSystemCapabilityTypeDisplays]) { + [self sdl_sendGetSystemCapabilityWithType:type subscribe:@YES completionHandler:nil]; + } else { + // If we're not calling the GSC RPC we should invoke the observer with the cached data + [self sdl_invokeObserver:observerObject withCapabilityType:type capability:[self sdl_cachedCapabilityForType:type] error:nil]; + } + } else { + // Store the observer and call it immediately with the cached value + [self.capabilityObservers[type] addObject:observerObject]; + [self sdl_invokeObserver:observerObject withCapabilityType:type capability:[self sdl_cachedCapabilityForType:type] error:nil]; + } return observerObject.observer; } -- (BOOL)subscribeToCapabilityType:(SDLSystemCapabilityType)type withObserver:(id)observer selector:(SEL)selector { - // DISPLAYS always works due to old-style SetDisplayLayoutRepsonse updates, but otherwise, subscriptions won't work - if (!self.supportsSubscriptions && ![type isEqualToEnum:SDLSystemCapabilityTypeDisplays]) { return NO; } +- (nullable id)subscribeToCapabilityType:(SDLSystemCapabilityType)type withUpdateHandler:(SDLCapabilityUpdateWithErrorHandler)handler { + SDLLogD(@"Subscribing to capability type: %@ with a handler", type); + SDLSystemCapabilityObserver *observerObject = [[SDLSystemCapabilityObserver alloc] initWithObserver:[[NSObject alloc] init] updateHandler:handler]; + + if ([self.currentHMILevel isEqualToEnum:SDLHMILevelNone] && ![type isEqualToEnum:SDLSystemCapabilityTypeDisplays]) { + SDLLogE(@"Attempted to subscribe to type: %@ in HMI level NONE, which is not allowed. Please wait until you are in HMI BACKGROUND, LIMITED, or FULL before attempting to subscribe to any SystemCapabilityType other than DISPLAYS.", type); + [self sdl_invokeObserver:observerObject withCapabilityType:type capability:nil error:[NSError sdl_systemCapabilityManager_cannotUpdateInHMINONE]]; + return nil; + } + + if (self.capabilityObservers[type] == nil) { + SDLLogD(@"This is the first subscription to capability type: %@, sending a GetSystemCapability with subscribe true", type); + self.capabilityObservers[type] = [NSMutableArray arrayWithObject:observerObject]; + // We don't want to send this for the displays type because that's automatically subscribed + if (![type isEqualToEnum:SDLSystemCapabilityTypeDisplays]) { + [self sdl_sendGetSystemCapabilityWithType:type subscribe:@YES completionHandler:nil]; + } else { + // If we're not calling the GSC RPC we should invoke the observer with the cached data + [self sdl_invokeObserver:observerObject withCapabilityType:type capability:[self sdl_cachedCapabilityForType:type] error:nil]; + } + } else { + // Store the observer and call it immediately with the cached value + [self.capabilityObservers[type] addObject:observerObject]; + [self sdl_invokeObserver:observerObject withCapabilityType:type capability:[self sdl_cachedCapabilityForType:type] error:nil]; + } + + return observerObject.observer; +} + +- (BOOL)subscribeToCapabilityType:(SDLSystemCapabilityType)type withObserver:(id)observer selector:(SEL)selector { + SDLLogD(@"Subscribing to capability type: %@, with observer: %@, selector: %@", type, observer, NSStringFromSelector(selector)); NSUInteger numberOfParametersInSelector = [NSStringFromSelector(selector) componentsSeparatedByString:@":"].count - 1; - if (numberOfParametersInSelector > 1) { return NO; } + if (numberOfParametersInSelector > 3) { + SDLLogE(@"Attempted to subscribe to a capability using a selector that contains more than 3 parameters."); + return NO; + } + + if (observer == nil) { + SDLLogE(@"Attempted to subscribe to type: %@ with a selector on a *nil* observer, which will always fail.", type); + return NO; + } SDLSystemCapabilityObserver *observerObject = [[SDLSystemCapabilityObserver alloc] initWithObserver:observer selector:selector]; - [self.capabilityObservers[type] addObject:observerObject]; + if ([self.currentHMILevel isEqualToEnum:SDLHMILevelNone] && ![type isEqualToEnum:SDLSystemCapabilityTypeDisplays]) { + SDLLogE(@"Attempted to subscribe to type: %@ in HMI level NONE, which is not allowed. Please wait until you are in HMI BACKGROUND, LIMITED, or FULL before attempting to subscribe to any SystemCapabilityType other than DISPLAYS.", type); + [self sdl_invokeObserver:observerObject withCapabilityType:type capability:nil error:[NSError sdl_systemCapabilityManager_cannotUpdateInHMINONE]]; + return NO; + } + + if (self.capabilityObservers[type] == nil) { + SDLLogD(@"This is the first subscription to capability type: %@, sending a GetSystemCapability with subscribe true", type); + self.capabilityObservers[type] = [NSMutableArray arrayWithObject:observerObject]; + + // We don't want to send this for the displays type because that's automatically subscribed + if (![type isEqualToEnum:SDLSystemCapabilityTypeDisplays]) { + [self sdl_sendGetSystemCapabilityWithType:type subscribe:@YES completionHandler:nil]; + } else { + // If we're not calling the GSC RPC we should invoke the observer with the cached data + [self sdl_invokeObserver:observerObject withCapabilityType:type capability:[self sdl_cachedCapabilityForType:type] error:nil]; + } + } else { + // Store the observer and call it immediately with the cached value + [self.capabilityObservers[type] addObject:observerObject]; + [self sdl_invokeObserver:observerObject withCapabilityType:type capability:[self sdl_cachedCapabilityForType:type] error:nil]; + } return YES; } - (void)unsubscribeFromCapabilityType:(SDLSystemCapabilityType)type withObserver:(id)observer { + SDLLogD(@"Unsubscribing from capability type: %@", type); for (SDLSystemCapabilityObserver *capabilityObserver in self.capabilityObservers[type]) { - if ([observer isEqual:capabilityObserver.observer]) { + if ([observer isEqual:capabilityObserver.observer] && self.capabilityObservers[type] != nil) { [self.capabilityObservers[type] removeObject:capabilityObserver]; + + [self sdl_removeNilObserversAndUnsubscribeIfNecessary]; break; } } @@ -602,31 +620,181 @@ - (void)unsubscribeFromCapabilityType:(SDLSystemCapabilityType)type withObserver /// Calls all observers of a capability type with an updated capability /// @param capability The new capability update /// @param handler The update handler to call, if one exists after the observers are called -- (void)sdl_callObserversForCapabilityUpdate:(SDLSystemCapability *)capability handler:(nullable SDLUpdateCapabilityHandler)handler { - for (SDLSystemCapabilityObserver *observer in self.capabilityObservers[capability.systemCapabilityType]) { - if (observer.block != nil) { - observer.block(capability); - } else { - NSUInteger numberOfParametersInSelector = [NSStringFromSelector(observer.selector) componentsSeparatedByString:@":"].count - 1; +- (void)sdl_callObserversForUpdate:(nullable SDLSystemCapability *)capability error:(nullable NSError *)error handler:(nullable SDLCapabilityUpdateWithErrorHandler)handler { + SDLSystemCapabilityType type = capability.systemCapabilityType; + SDLLogV(@"Calling observers for type: %@ with update: %@", type, capability); + + [self sdl_removeNilObserversAndUnsubscribeIfNecessary]; + + for (SDLSystemCapabilityObserver *observer in self.capabilityObservers[type]) { + [self sdl_invokeObserver:observer withCapabilityType:type capability:capability error:error]; + } + + if (handler == nil) { return; } + handler(capability, self.subscriptionStatus[type].boolValue, error); +} + +- (void)sdl_invokeObserver:(SDLSystemCapabilityObserver *)observer withCapabilityType:(SDLSystemCapabilityType)type capability:(nullable SDLSystemCapability *)capability error:(nullable NSError *)error { + BOOL subscribed = self.subscriptionStatus[type].boolValue || [type isEqualToEnum:SDLSystemCapabilityTypeDisplays]; + #pragma clang diagnostic push -#pragma clang diagnostic ignored "-Warc-performSelector-leaks" - if (numberOfParametersInSelector == 0) { - if ([observer.observer respondsToSelector:observer.selector]) { - [observer.observer performSelector:observer.selector]; - } - } else if (numberOfParametersInSelector == 1) { - if ([observer.observer respondsToSelector:observer.selector]) { - [observer.observer performSelector:observer.selector withObject:capability]; - } - } else { - @throw [NSException sdl_invalidSelectorExceptionWithSelector:observer.selector]; - } +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + if (observer.block != nil) { + observer.block(capability); #pragma clang diagnostic pop + } else if (observer.updateBlock != nil) { + observer.updateBlock(capability, subscribed, error); + } else { + if (![observer.observer respondsToSelector:observer.selector]) { + @throw [NSException sdl_invalidSelectorExceptionWithSelector:observer.selector]; + } + + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[(NSObject *)observer.observer methodSignatureForSelector:observer.selector]]; + [invocation setSelector:observer.selector]; + [invocation setTarget:observer.observer]; + + NSUInteger numberOfParametersInSelector = [NSStringFromSelector(observer.selector) componentsSeparatedByString:@":"].count - 1; + if (numberOfParametersInSelector >= 1) { + [invocation setArgument:&capability atIndex:2]; + } + if (numberOfParametersInSelector >= 2) { + [invocation setArgument:&error atIndex:3]; + } + if (numberOfParametersInSelector >= 3) { + [invocation setArgument:&subscribed atIndex:4]; + } + if (numberOfParametersInSelector >= 4) { + @throw [NSException sdl_invalidSelectorExceptionWithSelector:observer.selector]; + } + + [invocation invoke]; + } +} + +- (void)sdl_removeNilObserversAndUnsubscribeIfNecessary { + SDLLogV(@"Checking for nil observers and removing them, then checking for subscriptions we don't need and unsubscribing."); + // Loop through our observers + for (SDLSystemCapabilityType key in self.capabilityObservers.allKeys) { + for (SDLSystemCapabilityObserver *observer in self.capabilityObservers[key]) { + // If an observer object is nil, remove it + if (observer.observer == nil) { + [self.capabilityObservers[key] removeObject:observer]; + } + + // If we no longer have any observers for that type, remove the array + if (self.capabilityObservers[key].count == 0) { + [self.capabilityObservers removeObjectForKey:key]; + } } } - if (handler == nil) { return; } - handler(nil, self); + // If we don't support subscriptions, we don't want to unsubscribe by sending an RPC below + if (!self.supportsSubscriptions) { + return; + } + + // Loop through our subscription statuses, check if we're subscribed. If we are, and we do not have observers for that type, and that type is not DISPLAYS, then unsubscribe. + for (SDLSystemCapabilityType type in self.subscriptionStatus.allKeys) { + if ([self.subscriptionStatus[type] isEqualToNumber:@YES] + && self.capabilityObservers[type] == nil + && ![type isEqualToEnum:SDLSystemCapabilityTypeDisplays]) { + SDLLogD(@"Removing the last subscription to type %@, sending a GetSystemCapability with subscribe false (will unsubscribe)", type); + [self sdl_sendGetSystemCapabilityWithType:type subscribe:@NO completionHandler:nil]; + } + } +} + +#pragma mark - Notifications + +/// Registers for notifications and responses from Core +- (void)sdl_registerForNotifications { + SDLLogV(@"Registering for notifications"); + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sdl_registerResponse:) name:SDLDidReceiveRegisterAppInterfaceResponse object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sdl_displayLayoutResponse:) name:SDLDidReceiveSetDisplayLayoutResponse object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sdl_systemCapabilityUpdatedNotification:) name:SDLDidReceiveSystemCapabilityUpdatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sdl_hmiStatusNotification:) name:SDLDidChangeHMIStatusNotification object:nil]; +} + +/** + * Called when a `RegisterAppInterfaceResponse` response is received from Core. The head unit capabilities are saved. + * + * @param notification The `RegisterAppInterfaceResponse` response received from Core + */ +- (void)sdl_registerResponse:(SDLRPCResponseNotification *)notification { + SDLRegisterAppInterfaceResponse *response = (SDLRegisterAppInterfaceResponse *)notification.response; + if (!response.success.boolValue) { return; } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" + self.displayCapabilities = response.displayCapabilities; + self.softButtonCapabilities = response.softButtonCapabilities; + self.buttonCapabilities = response.buttonCapabilities; + self.presetBankCapabilities = response.presetBankCapabilities; +#pragma clang diagnostic pop + + self.hmiCapabilities = response.hmiCapabilities; + self.hmiZoneCapabilities = response.hmiZoneCapabilities; + self.speechCapabilities = response.speechCapabilities; + self.prerecordedSpeechCapabilities = response.prerecordedSpeech; + self.vrCapability = (response.vrCapabilities.count > 0 && [response.vrCapabilities.firstObject isEqualToEnum:SDLVRCapabilitiesText]) ? YES : NO; + self.audioPassThruCapabilities = response.audioPassThruCapabilities; + self.pcmStreamCapability = response.pcmStreamCapabilities; + + self.shouldConvertDeprecatedDisplayCapabilities = YES; + self.displays = [self sdl_createDisplayCapabilityListFromRegisterResponse:response]; + + SDLLogV(@"Received RegisterAppInterface response, filled out display and other capabilities"); + + // Call the observers in case the new display capability list is created from deprecated types + SDLSystemCapability *systemCapability = [[SDLSystemCapability alloc] initWithDisplayCapabilities:self.displays]; + [self sdl_callObserversForUpdate:systemCapability error:nil handler:nil]; +} + +/** + * Called when a `SetDisplayLayoutResponse` response is received from Core. If the template was set successfully, the the new capabilities for the template are saved. + * + * @param notification The `SetDisplayLayoutResponse` response received from Core + */ +- (void)sdl_displayLayoutResponse:(SDLRPCResponseNotification *)notification { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" + SDLSetDisplayLayoutResponse *response = (SDLSetDisplayLayoutResponse *)notification.response; +#pragma clang diagnostic pop + if (!response.success.boolValue) { return; } + + // If we've received a display capability update then we should not convert our deprecated display capabilities and we should just return + if (!self.shouldConvertDeprecatedDisplayCapabilities) { return; } + + self.displayCapabilities = response.displayCapabilities; + self.buttonCapabilities = response.buttonCapabilities; + self.softButtonCapabilities = response.softButtonCapabilities; + self.presetBankCapabilities = response.presetBankCapabilities; + + self.displays = [self sdl_createDisplayCapabilityListFromSetDisplayLayoutResponse:response]; + + SDLLogV(@"Received SetDisplayLayout response, filled out display and other capabilities"); + + // Call the observers in case the new display capability list is created from deprecated types + SDLSystemCapability *systemCapability = [[SDLSystemCapability alloc] initWithDisplayCapabilities:self.displays]; + [self sdl_callObserversForUpdate:systemCapability error:nil handler:nil]; +} + + +/** + * Called when an `OnSystemCapabilityUpdated` notification is received from Core. The updated system capabilty is saved. + * + * @param notification The `OnSystemCapabilityUpdated` notification received from Core + */ +- (void)sdl_systemCapabilityUpdatedNotification:(SDLRPCNotificationNotification *)notification { + SDLOnSystemCapabilityUpdated *systemCapabilityUpdatedNotification = (SDLOnSystemCapabilityUpdated *)notification.notification; + SDLLogV(@"Received OnSystemCapability update for type %@", systemCapabilityUpdatedNotification.systemCapability.systemCapabilityType); + + [self sdl_saveSystemCapability:systemCapabilityUpdatedNotification.systemCapability error:nil completionHandler:nil]; +} + +- (void)sdl_hmiStatusNotification:(SDLRPCNotificationNotification *)notification { + SDLOnHMIStatus *onHMIStatus = (SDLOnHMIStatus *)notification.notification; + self.currentHMILevel = onHMIStatus.hmiLevel; } @end diff --git a/SmartDeviceLink/SDLSystemCapabilityObserver.h b/SmartDeviceLink/SDLSystemCapabilityObserver.h index f5450d60e..11a6acfc1 100644 --- a/SmartDeviceLink/SDLSystemCapabilityObserver.h +++ b/SmartDeviceLink/SDLSystemCapabilityObserver.h @@ -12,8 +12,12 @@ NS_ASSUME_NONNULL_BEGIN +/// A handler mirroring the one in SDLSystemCapabilityManager.h for `initWithObserver:block:` typedef void (^SDLCapabilityUpdateHandler)(SDLSystemCapability *capability); +/// A handler mirroring the one in SDLSystemCapabilityManager.h for `initWithObserver:updateHandler:` +typedef void (^SDLCapabilityUpdateWithErrorHandler)(SDLSystemCapability *_Nullable capability, BOOL subscribed, NSError *_Nullable error); + /** An observer object for SDLSystemCapabilityManager */ @@ -32,7 +36,10 @@ typedef void (^SDLCapabilityUpdateHandler)(SDLSystemCapability *capability); /** A block called when the observer is triggered */ -@property (copy, nonatomic) SDLCapabilityUpdateHandler block; +@property (copy, nonatomic) SDLCapabilityUpdateHandler block __deprecated_msg("use updateBlock instead"); + +/// A block called when the observer is triggered +@property (copy, nonatomic) SDLCapabilityUpdateWithErrorHandler updateBlock; /** Create an observer using an object and a selector on that object @@ -50,7 +57,14 @@ typedef void (^SDLCapabilityUpdateHandler)(SDLSystemCapability *capability); @param block The block that will be called when the subscription triggers @return The observer */ -- (instancetype)initWithObserver:(id)observer block:(SDLCapabilityUpdateHandler)block; +- (instancetype)initWithObserver:(id)observer block:(SDLCapabilityUpdateHandler)block __deprecated_msg("use initWithObserver:updateHandler: instead"); + +/// Create an observer using an object and a callback block + +/// @param observer The object that can be used to unsubscribe the block +/// @param block The block that will be called when the subscription triggers +/// @return The observer +- (instancetype)initWithObserver:(id)observer updateHandler:(SDLCapabilityUpdateWithErrorHandler)block; @end diff --git a/SmartDeviceLink/SDLSystemCapabilityObserver.m b/SmartDeviceLink/SDLSystemCapabilityObserver.m index 7ccbd38af..7b6ef0b0e 100644 --- a/SmartDeviceLink/SDLSystemCapabilityObserver.m +++ b/SmartDeviceLink/SDLSystemCapabilityObserver.m @@ -32,6 +32,16 @@ - (instancetype)initWithObserver:(id)observer block:(SDLCapabilityUpda return self; } +- (instancetype)initWithObserver:(id)observer updateHandler:(SDLCapabilityUpdateWithErrorHandler)block { + self = [super init]; + if (!self) { return nil; } + + _observer = observer; + _updateBlock = block; + + return self; +} + - (NSString *)description { if (self.selector) { return [NSString stringWithFormat:@"Observer: %@[%@] - %@", [_observer class], _observer, NSStringFromSelector(_selector)]; diff --git a/SmartDeviceLinkTests/DevAPISpecs/TestSystemCapabilityObserver.h b/SmartDeviceLinkTests/DevAPISpecs/TestSystemCapabilityObserver.h index 0e6c8bcbb..1d602f1f3 100644 --- a/SmartDeviceLinkTests/DevAPISpecs/TestSystemCapabilityObserver.h +++ b/SmartDeviceLinkTests/DevAPISpecs/TestSystemCapabilityObserver.h @@ -8,16 +8,21 @@ #import -@class SDLSystemCapabilityManager; +@class SDLSystemCapability; NS_ASSUME_NONNULL_BEGIN @interface TestSystemCapabilityObserver : NSObject @property (assign, nonatomic) NSUInteger selectorCalledCount; +@property (strong, nonatomic, nullable) NSMutableArray *capabilitiesReceived; +@property (strong, nonatomic, nullable) NSMutableArray *errorsReceived; +@property (strong, nonatomic, nullable) NSMutableArray *subscribedValuesReceived; - (void)capabilityUpdated; -- (void)capabilityUpdatedWithNotification:(SDLSystemCapabilityManager *)capabilityManager; +- (void)capabilityUpdatedWithCapability:(SDLSystemCapability *)capability; +- (void)capabilityUpdatedWithCapability:(SDLSystemCapability *)capability error:(NSError *)error; +- (void)capabilityUpdatedWithCapability:(SDLSystemCapability *)capability error:(NSError *)error subscribed:(BOOL)subscribed; @end diff --git a/SmartDeviceLinkTests/DevAPISpecs/TestSystemCapabilityObserver.m b/SmartDeviceLinkTests/DevAPISpecs/TestSystemCapabilityObserver.m index 5366b0884..02696cb1b 100644 --- a/SmartDeviceLinkTests/DevAPISpecs/TestSystemCapabilityObserver.m +++ b/SmartDeviceLinkTests/DevAPISpecs/TestSystemCapabilityObserver.m @@ -8,6 +8,8 @@ #import "TestSystemCapabilityObserver.h" +#import "SDLSystemCapability.h" + @implementation TestSystemCapabilityObserver - (instancetype)init { @@ -15,6 +17,9 @@ - (instancetype)init { if (!self) { return nil; } _selectorCalledCount = 0; + _capabilitiesReceived = [NSMutableArray array]; + _errorsReceived = [NSMutableArray array]; + _subscribedValuesReceived = [NSMutableArray array]; return self; } @@ -23,8 +28,38 @@ - (void)capabilityUpdated { self.selectorCalledCount++; } -- (void)capabilityUpdatedWithNotification:(SDLSystemCapabilityManager *)capabilityManager { +- (void)capabilityUpdatedWithCapability:(SDLSystemCapability *)capability { + self.selectorCalledCount++; + + if (capability != nil) { + [self.capabilitiesReceived addObject:capability]; + } +} + +- (void)capabilityUpdatedWithCapability:(SDLSystemCapability *)capability error:(NSError *)error { + self.selectorCalledCount++; + + if (capability != nil) { + [self.capabilitiesReceived addObject:capability]; + } + + if (error != nil) { + [self.errorsReceived addObject:error]; + } +} + +- (void)capabilityUpdatedWithCapability:(SDLSystemCapability *)capability error:(NSError *)error subscribed:(BOOL)subscribed { self.selectorCalledCount++; + + if (capability != nil) { + [self.capabilitiesReceived addObject:capability]; + } + + if (error != nil) { + [self.errorsReceived addObject:error]; + } + + [self.subscribedValuesReceived addObject:@(subscribed)]; } @end diff --git a/SmartDeviceLinkTests/SDLSystemCapabilityManagerSpec.m b/SmartDeviceLinkTests/SDLSystemCapabilityManagerSpec.m index e114328e0..5ce732c86 100644 --- a/SmartDeviceLinkTests/SDLSystemCapabilityManagerSpec.m +++ b/SmartDeviceLinkTests/SDLSystemCapabilityManagerSpec.m @@ -11,6 +11,7 @@ #import "SDLDisplayCapability.h" #import "SDLGetSystemCapability.h" #import "SDLGetSystemCapabilityResponse.h" +#import "SDLGlobals.h" #import "SDLHMICapabilities.h" #import "SDLImageField.h" #import "SDLImageResolution.h" @@ -30,18 +31,50 @@ #import "SDLSetDisplayLayoutResponse.h" #import "SDLSoftButtonCapabilities.h" #import "SDLSystemCapability.h" +#import "SDLSystemCapabilityObserver.h" #import "SDLSystemCapabilityManager.h" #import "SDLTextField.h" +#import "SDLVersion.h" #import "SDLVideoStreamingCapability.h" #import "SDLWindowCapability.h" #import "SDLWindowTypeCapabilities.h" #import "TestConnectionManager.h" #import "TestSystemCapabilityObserver.h" +typedef NSString * SDLServiceID; @interface SDLSystemCapabilityManager () +@property (weak, nonatomic) id connectionManager; + +@property (nullable, strong, nonatomic, readwrite) NSArray *displays; +@property (nullable, strong, nonatomic, readwrite) SDLDisplayCapabilities *displayCapabilities; +@property (nullable, strong, nonatomic, readwrite) SDLHMICapabilities *hmiCapabilities; +@property (nullable, copy, nonatomic, readwrite) NSArray *softButtonCapabilities; +@property (nullable, copy, nonatomic, readwrite) NSArray *buttonCapabilities; +@property (nullable, strong, nonatomic, readwrite) SDLPresetBankCapabilities *presetBankCapabilities; +@property (nullable, copy, nonatomic, readwrite) NSArray *hmiZoneCapabilities; +@property (nullable, copy, nonatomic, readwrite) NSArray *speechCapabilities; +@property (nullable, copy, nonatomic, readwrite) NSArray *prerecordedSpeechCapabilities; +@property (nonatomic, assign, readwrite) BOOL vrCapability; +@property (nullable, copy, nonatomic, readwrite) NSArray *audioPassThruCapabilities; +@property (nullable, strong, nonatomic, readwrite) SDLAudioPassThruCapabilities *pcmStreamCapability; +@property (nullable, strong, nonatomic, readwrite) SDLNavigationCapability *navigationCapability; +@property (nullable, strong, nonatomic, readwrite) SDLPhoneCapability *phoneCapability; +@property (nullable, strong, nonatomic, readwrite) SDLVideoStreamingCapability *videoStreamingCapability; +@property (nullable, strong, nonatomic, readwrite) SDLRemoteControlCapabilities *remoteControlCapability; +@property (nullable, strong, nonatomic, readwrite) SDLSeatLocationCapability *seatLocationCapability; + +@property (nullable, strong, nonatomic) NSMutableDictionary *appServicesCapabilitiesDictionary; + @property (assign, nonatomic, readwrite) BOOL supportsSubscriptions; +@property (strong, nonatomic) NSMutableDictionary *> *capabilityObservers; +@property (strong, nonatomic) NSMutableDictionary *> *subscriptionStatus; + +@property (nullable, strong, nonatomic) SDLSystemCapability *lastReceivedCapability; + +@property (assign, nonatomic) BOOL shouldConvertDeprecatedDisplayCapabilities; +@property (strong, nonatomic) SDLHMILevel currentHMILevel; @end @@ -139,7 +172,154 @@ @interface SDLSystemCapabilityManager () expect(testSystemCapabilityManager.remoteControlCapability).to(beNil()); expect(testSystemCapabilityManager.appServicesCapabilities).to(beNil()); expect(testSystemCapabilityManager.seatLocationCapability).to(beNil()); + expect(testSystemCapabilityManager.currentHMILevel).to(equal(SDLHMILevelNone)); + }); + + describe(@"isCapabilitySupported method should work correctly", ^{ + __block SDLHMICapabilities *hmiCapabilities = nil; + + beforeEach(^{ + hmiCapabilities = [[SDLHMICapabilities alloc] init]; + }); + + context(@"when there's a cached phone capability and hmiCapabilities is nil", ^{ + beforeEach(^{ + testSystemCapabilityManager.phoneCapability = [[SDLPhoneCapability alloc] initWithDialNumber:YES]; + }); + + it(@"should return true", ^{ + expect([testSystemCapabilityManager isCapabilitySupported:SDLSystemCapabilityTypePhoneCall]).to(beTrue()); + }); + }); + + context(@"when there's no cached capability", ^{ + describe(@"pulling a phone capability when HMICapabilites.phoneCapability is false", ^{ + beforeEach(^{ + hmiCapabilities.phoneCall = @NO; + testSystemCapabilityManager.hmiCapabilities = hmiCapabilities; + }); + + it(@"should return NO", ^{ + expect([testSystemCapabilityManager isCapabilitySupported:SDLSystemCapabilityTypePhoneCall]).to(beFalse()); + }); + }); + + describe(@"pulling a phone capability when HMICapabilites.phoneCapability is true", ^{ + beforeEach(^{ + hmiCapabilities.phoneCall = @YES; + testSystemCapabilityManager.hmiCapabilities = hmiCapabilities; + }); + + it(@"should return NO", ^{ + expect([testSystemCapabilityManager isCapabilitySupported:SDLSystemCapabilityTypePhoneCall]).to(beTrue()); + }); + }); + + describe(@"pulling a phone capability when HMICapabilites.phoneCapability is nil", ^{ + it(@"should return NO", ^{ + expect([testSystemCapabilityManager isCapabilitySupported:SDLSystemCapabilityTypePhoneCall]).to(beFalse()); + }); + }); + + describe(@"pulling an app services capability", ^{ + context(@"on RPC connection version 5.1.0 and HMICapabilities.appServices is false", ^{ + beforeEach(^{ + [SDLGlobals sharedGlobals].rpcVersion = [SDLVersion versionWithString:@"5.1.0"]; + hmiCapabilities.appServices = @NO; + testSystemCapabilityManager.hmiCapabilities = hmiCapabilities; + }); + + it(@"should return true", ^{ + expect([testSystemCapabilityManager isCapabilitySupported:SDLSystemCapabilityTypeAppServices]).to(beTrue()); + }); + }); + + context(@"on RPC connection version 5.2.0 and HMICapabilities.appServices is false", ^{ + beforeEach(^{ + [SDLGlobals sharedGlobals].rpcVersion = [SDLVersion versionWithString:@"5.2.0"]; + hmiCapabilities.appServices = @NO; + testSystemCapabilityManager.hmiCapabilities = hmiCapabilities; + }); + + it(@"should return false", ^{ + expect([testSystemCapabilityManager isCapabilitySupported:SDLSystemCapabilityTypeAppServices]).to(beFalse()); + }); + }); + }); + + describe(@"pulling a video streaming capability", ^{ + context(@"on RPC connection version 2.0.0 and HMICapabilities is nil", ^{ + beforeEach(^{ + [SDLGlobals sharedGlobals].rpcVersion = [SDLVersion versionWithString:@"2.0.0"]; + }); + + it(@"should return false", ^{ + expect([testSystemCapabilityManager isCapabilitySupported:SDLSystemCapabilityTypeVideoStreaming]).to(beFalse()); + }); + }); + + context(@"on RPC connection version 3.0.0 and HMICapabilities is nil", ^{ + beforeEach(^{ + [SDLGlobals sharedGlobals].rpcVersion = [SDLVersion versionWithString:@"3.0.0"]; + }); + + context(@"when displayCapabilities.graphicSupported is true", ^{ + beforeEach(^{ + testSystemCapabilityManager.displayCapabilities = [[SDLDisplayCapabilities alloc] init]; + testSystemCapabilityManager.displayCapabilities.graphicSupported = @YES; + }); + + it(@"should return true", ^{ + expect([testSystemCapabilityManager isCapabilitySupported:SDLSystemCapabilityTypeVideoStreaming]).to(beTrue()); + }); + }); + + context(@"when displayCapabilities.graphicSupported is false", ^{ + beforeEach(^{ + testSystemCapabilityManager.displayCapabilities.graphicSupported = @NO; + }); + + it(@"should return true", ^{ + expect([testSystemCapabilityManager isCapabilitySupported:SDLSystemCapabilityTypeVideoStreaming]).to(beFalse()); + }); + }); + }); + context(@"on RPC connection version 5.1.0", ^{ + beforeEach(^{ + [SDLGlobals sharedGlobals].rpcVersion = [SDLVersion versionWithString:@"5.1.0"]; + }); + + context(@"when HMICapabilites.videoStreaming is false", ^{ + beforeEach(^{ + hmiCapabilities.videoStreaming = @NO; + testSystemCapabilityManager.hmiCapabilities = hmiCapabilities; + }); + + it(@"should return false", ^{ + expect([testSystemCapabilityManager isCapabilitySupported:SDLSystemCapabilityTypeVideoStreaming]).to(beFalse()); + }); + }); + + context(@"when HMICapabilites.videoStreaming is true", ^{ + beforeEach(^{ + hmiCapabilities.videoStreaming = @YES; + testSystemCapabilityManager.hmiCapabilities = hmiCapabilities; + }); + + it(@"should return true", ^{ + expect([testSystemCapabilityManager isCapabilitySupported:SDLSystemCapabilityTypeVideoStreaming]).to(beTrue()); + }); + }); + + context(@"when HMICapabilites.videoStreaming is nil", ^{ + it(@"should return false", ^{ + expect([testSystemCapabilityManager isCapabilitySupported:SDLSystemCapabilityTypeVideoStreaming]).to(beFalse()); + }); + }); + }); + }); + }); }); context(@"When notified of a register app interface response", ^{ @@ -355,7 +535,7 @@ @interface SDLSystemCapabilityManager () }); }); - context(@"When sending a GetSystemCapability request", ^{ + context(@"When sending a updateCapabilityType request in HMI FULL", ^{ __block SDLGetSystemCapabilityResponse *testGetSystemCapabilityResponse = nil; __block SDLPhoneCapability *testPhoneCapability = nil; @@ -366,6 +546,8 @@ @interface SDLSystemCapabilityManager () testGetSystemCapabilityResponse.systemCapability = [[SDLSystemCapability alloc] init]; testGetSystemCapabilityResponse.systemCapability.phoneCapability = testPhoneCapability; testGetSystemCapabilityResponse.systemCapability.systemCapabilityType = SDLSystemCapabilityTypePhoneCall; + + testSystemCapabilityManager.currentHMILevel = SDLHMILevelFull; }); context(@"If the request failed with an error", ^{ @@ -377,17 +559,14 @@ @interface SDLSystemCapabilityManager () }); it(@"should should not save the capabilities", ^{ - waitUntilTimeout(1, ^(void (^done)(void)) { - [testSystemCapabilityManager updateCapabilityType:testGetSystemCapabilityResponse.systemCapability.systemCapabilityType completionHandler:^(NSError * _Nullable error, SDLSystemCapabilityManager * _Nonnull systemCapabilityManager) { - expect(error).to(equal(testConnectionManager.defaultError)); - expect(systemCapabilityManager.phoneCapability).to(beNil()); - done(); - }]; + [testSystemCapabilityManager updateCapabilityType:SDLSystemCapabilityTypePhoneCall completionHandler:^(NSError * _Nullable error, SDLSystemCapabilityManager * _Nonnull systemCapabilityManager) { + expect(error).toEventually(equal(testConnectionManager.defaultError)); + expect(systemCapabilityManager.phoneCapability).toEventually(beNil()); + }]; - [NSThread sleepForTimeInterval:0.1]; + [NSThread sleepForTimeInterval:0.1]; - [testConnectionManager respondToLastRequestWithResponse:testGetSystemCapabilityResponse]; - }); + [testConnectionManager respondToLastRequestWithResponse:testGetSystemCapabilityResponse]; }); }); @@ -397,17 +576,14 @@ @interface SDLSystemCapabilityManager () }); it(@"should save the capabilitity", ^{ - waitUntilTimeout(1, ^(void (^done)(void)){ - [testSystemCapabilityManager updateCapabilityType:testGetSystemCapabilityResponse.systemCapability.systemCapabilityType completionHandler:^(NSError * _Nullable error, SDLSystemCapabilityManager * _Nonnull systemCapabilityManager) { - expect(testSystemCapabilityManager.phoneCapability).to(equal(testPhoneCapability)); - expect(error).to(beNil()); - done(); - }]; + [testSystemCapabilityManager updateCapabilityType:SDLSystemCapabilityTypePhoneCall completionHandler:^(NSError * _Nullable error, SDLSystemCapabilityManager * _Nonnull systemCapabilityManager) { + expect(testSystemCapabilityManager.phoneCapability).toEventually(equal(testPhoneCapability)); + expect(error).toEventually(beNil()); + }]; - [NSThread sleepForTimeInterval:0.1]; + [NSThread sleepForTimeInterval:0.1]; - [testConnectionManager respondToLastRequestWithResponse:testGetSystemCapabilityResponse]; - }); + [testConnectionManager respondToLastRequestWithResponse:testGetSystemCapabilityResponse]; }); }); @@ -435,10 +611,12 @@ @interface SDLSystemCapabilityManager () }); }); - describe(@"updating the SCM through OnSystemCapability", ^{ + describe(@"updating the SCM through OnSystemCapability in HMI Full", ^{ __block SDLPhoneCapability *phoneCapability = nil; beforeEach(^{ + testSystemCapabilityManager.currentHMILevel = SDLHMILevelFull; + phoneCapability = [[SDLPhoneCapability alloc] initWithDialNumber:YES]; SDLSystemCapability *newCapability = [[SDLSystemCapability alloc] initWithPhoneCapability:phoneCapability]; SDLOnSystemCapabilityUpdated *update = [[SDLOnSystemCapabilityUpdated alloc] initWithSystemCapability:newCapability]; @@ -452,42 +630,60 @@ @interface SDLSystemCapabilityManager () }); }); - describe(@"subscribing to capability types", ^{ + describe(@"subscribing to capability types when HMI is full", ^{ __block TestSystemCapabilityObserver *phoneObserver = nil; __block TestSystemCapabilityObserver *navigationObserver = nil; + __block TestSystemCapabilityObserver *videoStreamingObserver = nil; + __block TestSystemCapabilityObserver *displaysObserver = nil; - __block NSUInteger blockObserverTriggeredCount = 0; + __block NSUInteger observerTriggeredCount = 0; + __block NSUInteger handlerTriggeredCount = 0; beforeEach(^{ - blockObserverTriggeredCount = 0; - testSystemCapabilityManager.supportsSubscriptions = YES; + testSystemCapabilityManager.currentHMILevel = SDLHMILevelFull; + + observerTriggeredCount = 0; + handlerTriggeredCount = 0; + [SDLGlobals sharedGlobals].rpcVersion = [SDLVersion versionWithString:@"5.1.0"]; // supports subscriptions phoneObserver = [[TestSystemCapabilityObserver alloc] init]; - [testSystemCapabilityManager subscribeToCapabilityType:SDLSystemCapabilityTypePhoneCall withObserver:phoneObserver selector:@selector(capabilityUpdatedWithNotification:)]; + [testSystemCapabilityManager subscribeToCapabilityType:SDLSystemCapabilityTypePhoneCall withObserver:phoneObserver selector:@selector(capabilityUpdatedWithCapability:)]; navigationObserver = [[TestSystemCapabilityObserver alloc] init]; - [testSystemCapabilityManager subscribeToCapabilityType:SDLSystemCapabilityTypeNavigation withObserver:navigationObserver selector:@selector(capabilityUpdatedWithNotification:)]; + [testSystemCapabilityManager subscribeToCapabilityType:SDLSystemCapabilityTypeNavigation withObserver:navigationObserver selector:@selector(capabilityUpdatedWithCapability:error:)]; + videoStreamingObserver = [[TestSystemCapabilityObserver alloc] init]; + [testSystemCapabilityManager subscribeToCapabilityType:SDLSystemCapabilityTypeVideoStreaming withObserver:videoStreamingObserver selector:@selector(capabilityUpdatedWithCapability:error:subscribed:)]; + displaysObserver = [[TestSystemCapabilityObserver alloc] init]; + [testSystemCapabilityManager subscribeToCapabilityType:SDLSystemCapabilityTypeDisplays withObserver:displaysObserver selector:@selector(capabilityUpdatedWithCapability:error:subscribed:)]; }); - describe(@"when observers aren't supported", ^{ + context(@"when observers aren't supported", ^{ __block BOOL observationSuccess = NO; beforeEach(^{ - testSystemCapabilityManager.supportsSubscriptions = NO; + [SDLGlobals sharedGlobals].rpcVersion = [SDLVersion versionWithString:@"5.0.0"]; // no subscriptions - observationSuccess = [testSystemCapabilityManager subscribeToCapabilityType:SDLSystemCapabilityTypePhoneCall withObserver:phoneObserver selector:@selector(capabilityUpdatedWithNotification:)]; + observationSuccess = [testSystemCapabilityManager subscribeToCapabilityType:SDLSystemCapabilityTypePhoneCall withObserver:phoneObserver selector:@selector(capabilityUpdatedWithCapability:)]; }); it(@"should fail to subscribe", ^{ - expect(observationSuccess).to(beFalse()); + expect(observationSuccess).to(beTrue()); }); }); context(@"from a GetSystemCapabilitiesResponse", ^{ __block id blockObserver = nil; + __block id handlerObserver = nil; beforeEach(^{ + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" blockObserver = [testSystemCapabilityManager subscribeToCapabilityType:SDLSystemCapabilityTypePhoneCall withBlock:^(SDLSystemCapability * _Nonnull systemCapability) { - blockObserverTriggeredCount++; + observerTriggeredCount++; + }]; +#pragma clang diagnostic pop + + handlerObserver = [testSystemCapabilityManager subscribeToCapabilityType:SDLSystemCapabilityTypePhoneCall withUpdateHandler:^(SDLSystemCapability * _Nullable capability, BOOL subscribed, NSError * _Nullable error) { + handlerTriggeredCount++; }]; SDLGetSystemCapabilityResponse *testResponse = [[SDLGetSystemCapabilityResponse alloc] init]; @@ -497,15 +693,27 @@ @interface SDLSystemCapabilityManager () [[NSNotificationCenter defaultCenter] postNotification:notification]; }); - it(@"should notify subscribers of the new data", ^{ + it(@"should not notify subscribers of new data because it was sent outside of the SCM", ^{ + expect(handlerTriggeredCount).toEventually(equal(1)); + expect(observerTriggeredCount).toEventually(equal(1)); + expect(phoneObserver.selectorCalledCount).toEventually(equal(1)); - expect(blockObserverTriggeredCount).toEventually(equal(1)); - expect(navigationObserver.selectorCalledCount).toEventually(equal(0)); + + expect(navigationObserver.selectorCalledCount).toEventually(equal(1)); + + expect(videoStreamingObserver.selectorCalledCount).toEventually(equal(1)); + expect(videoStreamingObserver.subscribedValuesReceived).toEventually(haveCount(1)); + expect(videoStreamingObserver.subscribedValuesReceived.firstObject).toEventually(beFalse()); + + expect(displaysObserver.selectorCalledCount).toEventually(equal(1)); + expect(displaysObserver.subscribedValuesReceived).toEventually(haveCount(1)); + expect(displaysObserver.subscribedValuesReceived.firstObject).toEventually(beTrue()); }); describe(@"unsubscribing", ^{ beforeEach(^{ [testSystemCapabilityManager unsubscribeFromCapabilityType:SDLSystemCapabilityTypePhoneCall withObserver:phoneObserver]; + [testSystemCapabilityManager unsubscribeFromCapabilityType:SDLSystemCapabilityTypePhoneCall withObserver:handlerObserver]; [testSystemCapabilityManager unsubscribeFromCapabilityType:SDLSystemCapabilityTypePhoneCall withObserver:blockObserver]; SDLGetSystemCapabilityResponse *testResponse = [[SDLGetSystemCapabilityResponse alloc] init]; @@ -516,19 +724,34 @@ @interface SDLSystemCapabilityManager () }); it(@"should not notify the subscriber of the new data", ^{ + expect(handlerTriggeredCount).toEventually(equal(1)); + expect(observerTriggeredCount).toEventually(equal(1)); + expect(phoneObserver.selectorCalledCount).toEventually(equal(1)); // No change from above - expect(blockObserverTriggeredCount).toEventually(equal(1)); - expect(navigationObserver.selectorCalledCount).toEventually(equal(0)); + + expect(navigationObserver.selectorCalledCount).toEventually(equal(1)); + + expect(videoStreamingObserver.selectorCalledCount).toEventually(equal(1)); + + expect(displaysObserver.selectorCalledCount).toEventually(equal(1)); }); }); }); context(@"from an OnSystemCapabilities notification", ^{ __block id blockObserver = nil; + __block id handlerObserver = nil; beforeEach(^{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" blockObserver = [testSystemCapabilityManager subscribeToCapabilityType:SDLSystemCapabilityTypePhoneCall withBlock:^(SDLSystemCapability * _Nonnull systemCapability) { - blockObserverTriggeredCount++; + observerTriggeredCount++; + }]; +#pragma clang diagnostic pop + + handlerObserver = [testSystemCapabilityManager subscribeToCapabilityType:SDLSystemCapabilityTypePhoneCall withUpdateHandler:^(SDLSystemCapability * _Nullable capability, BOOL subscribed, NSError * _Nullable error) { + handlerTriggeredCount++; }]; SDLOnSystemCapabilityUpdated *testNotification = [[SDLOnSystemCapabilityUpdated alloc] initWithSystemCapability:[[SDLSystemCapability alloc] initWithPhoneCapability:[[SDLPhoneCapability alloc] initWithDialNumber:YES]]]; @@ -538,9 +761,20 @@ @interface SDLSystemCapabilityManager () }); it(@"should notify subscribers of the new data", ^{ - expect(phoneObserver.selectorCalledCount).toEventually(equal(1)); - expect(blockObserverTriggeredCount).toEventually(equal(1)); - expect(navigationObserver.selectorCalledCount).toEventually(equal(0)); + expect(handlerTriggeredCount).toEventually(equal(2)); + expect(observerTriggeredCount).toEventually(equal(2)); + + expect(phoneObserver.selectorCalledCount).toEventually(equal(2)); + + expect(navigationObserver.selectorCalledCount).toEventually(equal(1)); + + expect(videoStreamingObserver.selectorCalledCount).toEventually(equal(1)); + expect(videoStreamingObserver.subscribedValuesReceived).toEventually(haveCount(1)); + expect(videoStreamingObserver.subscribedValuesReceived.firstObject).toEventually(beFalse()); + + expect(displaysObserver.selectorCalledCount).toEventually(equal(1)); + expect(displaysObserver.subscribedValuesReceived).toEventually(haveCount(1)); + expect(displaysObserver.subscribedValuesReceived.firstObject).toEventually(beTrue()); }); describe(@"unsubscribing", ^{ @@ -556,9 +790,11 @@ @interface SDLSystemCapabilityManager () }); it(@"should not notify the subscriber of the new data", ^{ - expect(phoneObserver.selectorCalledCount).toEventually(equal(1)); // No change from above - expect(blockObserverTriggeredCount).toEventually(equal(1)); - expect(navigationObserver.selectorCalledCount).toEventually(equal(0)); + expect(phoneObserver.selectorCalledCount).toEventually(equal(2)); // No change from above + expect(observerTriggeredCount).toEventually(equal(2)); + expect(navigationObserver.selectorCalledCount).toEventually(equal(1)); + expect(videoStreamingObserver.selectorCalledCount).toEventually(equal(1)); + expect(displaysObserver.selectorCalledCount).toEventually(equal(1)); }); }); }); @@ -625,20 +861,6 @@ @interface SDLSystemCapabilityManager () }); }); - describe(@"when entering HMI FULL", ^{ - beforeEach(^{ - SDLOnHMIStatus *fullStatus = [[SDLOnHMIStatus alloc] init]; - fullStatus.hmiLevel = SDLHMILevelFull; - SDLRPCNotificationNotification *notification = [[SDLRPCNotificationNotification alloc] initWithName:SDLDidChangeHMIStatusNotification object:nil rpcNotification:fullStatus]; - [[NSNotificationCenter defaultCenter] postNotification:notification]; - }); - - it(@"should send GetSystemCapability subscriptions for all known capabilities", ^{ - expect(testConnectionManager.receivedRequests).to(haveCount(7)); - expect(testConnectionManager.receivedRequests.lastObject).to(beAnInstanceOf([SDLGetSystemCapability class])); - }); - }); - describe(@"when the system capability manager is stopped after being started", ^{ beforeEach(^{ [testSystemCapabilityManager stop];