diff options
author | Joel Fischer <joeljfischer@gmail.com> | 2022-08-10 15:54:35 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-10 15:54:35 -0400 |
commit | aba5ba83d902a1c5e7e68915180eaae319855430 (patch) | |
tree | 54ae427cd27fac366318c6e42606759e2de8e9ba | |
parent | 42393ce4226a3a1742a164a7be1c8045a07b215f (diff) | |
parent | b270c572be22de19376c74b2d8a98206926ef2e7 (diff) | |
download | sdl_ios-aba5ba83d902a1c5e7e68915180eaae319855430.tar.gz |
Merge pull request #2102 from smartdevicelink/bestpractice/issue-2101-remote-example
Add a Remote Control Example to the Example Apps
-rw-r--r-- | Example Apps/Example ObjC/MenuManager.h | 3 | ||||
-rw-r--r-- | Example Apps/Example ObjC/MenuManager.m | 55 | ||||
-rw-r--r-- | Example Apps/Example ObjC/ProxyManager.m | 28 | ||||
-rw-r--r-- | Example Apps/Example ObjC/RemoteControlManager.h | 29 | ||||
-rw-r--r-- | Example Apps/Example ObjC/RemoteControlManager.m | 247 | ||||
-rw-r--r-- | Example Apps/Example Swift/MenuManager.swift | 58 | ||||
-rw-r--r-- | Example Apps/Example Swift/ProxyManager.swift | 25 | ||||
-rw-r--r-- | Example Apps/Example Swift/RemoteControlManager.swift | 226 | ||||
-rw-r--r-- | Example Apps/Shared/AppConstants.h | 5 | ||||
-rw-r--r-- | Example Apps/Shared/AppConstants.m | 5 | ||||
-rw-r--r-- | Example Apps/Shared/Images.xcassets/remote_control.imageset/Contents.json | 21 | ||||
-rw-r--r-- | Example Apps/Shared/Images.xcassets/remote_control.imageset/remote_control_icon.png | bin | 0 -> 2146 bytes | |||
-rw-r--r-- | SmartDeviceLink-iOS.xcodeproj/project.pbxproj | 12 | ||||
-rw-r--r-- | SmartDeviceLink-iOS.xcodeproj/xcshareddata/xcschemes/SmartDeviceLinkTests.xcscheme | 52 |
14 files changed, 744 insertions, 22 deletions
diff --git a/Example Apps/Example ObjC/MenuManager.h b/Example Apps/Example ObjC/MenuManager.h index a7c1404c1..a78b2d077 100644 --- a/Example Apps/Example ObjC/MenuManager.h +++ b/Example Apps/Example ObjC/MenuManager.h @@ -9,6 +9,7 @@ #import <Foundation/Foundation.h> @class PerformInteractionManager; +@class RemoteControlManager; @class SDLManager; @class SDLMenuCell; @class SDLVoiceCommand; @@ -17,7 +18,7 @@ NS_ASSUME_NONNULL_BEGIN @interface MenuManager : NSObject -+ (NSArray<SDLMenuCell *> *)allMenuItemsWithManager:(SDLManager *)manager performManager:(PerformInteractionManager *)performManager; ++ (NSArray<SDLMenuCell *> *)allMenuItemsWithManager:(SDLManager *)manager performManager:(PerformInteractionManager *)performManager remoteManager:(RemoteControlManager *)remoteManager; + (NSArray<SDLVoiceCommand *> *)allVoiceMenuItemsWithManager:(SDLManager *)manager; @end diff --git a/Example Apps/Example ObjC/MenuManager.m b/Example Apps/Example ObjC/MenuManager.m index d78be2b8f..29d5986fa 100644 --- a/Example Apps/Example ObjC/MenuManager.m +++ b/Example Apps/Example ObjC/MenuManager.m @@ -7,6 +7,7 @@ // #import "MenuManager.h" + #import "AlertManager.h" #import "AudioManager.h" #import "AppConstants.h" @@ -14,14 +15,16 @@ #import "RPCPermissionsManager.h" #import "SmartDeviceLink.h" #import "VehicleDataManager.h" +#import "RemoteControlManager.h" NS_ASSUME_NONNULL_BEGIN @implementation MenuManager -+ (NSArray<SDLMenuCell *> *)allMenuItemsWithManager:(SDLManager *)manager performManager:(PerformInteractionManager *)performManager { ++ (NSArray<SDLMenuCell *> *)allMenuItemsWithManager:(SDLManager *)manager performManager:(PerformInteractionManager *)performManager remoteManager:(RemoteControlManager *)remoteControlManager { return @[[self sdlex_menuCellSpeakNameWithManager:manager], [self sdlex_menuCellGetAllVehicleDataWithManager:manager], + [self sdlex_menuCellRemoteWithManager:manager remoteManager:remoteControlManager], [self sdlex_menuCellShowPerformInteractionWithManager:manager performManager:performManager], [self sdlex_sliderMenuCellWithManager:manager], [self sdlex_scrollableMessageMenuCellWithManager:manager], @@ -91,7 +94,7 @@ NS_ASSUME_NONNULL_BEGIN + (SDLMenuCell *)sdlex_menuCellChangeTemplateWithManager:(SDLManager *)manager { - /// Lets give an example of 2 templates + // Lets give an example of 2 templates NSMutableArray *submenuItems = [NSMutableArray array]; NSString *errorMessage = @"Changing the template failed"; @@ -134,7 +137,7 @@ NS_ASSUME_NONNULL_BEGIN return [[SDLMenuCell alloc] initWithTitle:ACSliderMenuName secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:@[ACSliderMenuName] handler:^(SDLTriggerSource _Nonnull triggerSource) { SDLSlider *sliderRPC = [[SDLSlider alloc] initWithNumTicks:3 position:1 sliderHeader:@"Select a letter" sliderFooters:@[@"A", @"B", @"C"] timeout:10000]; [manager sendRequest:sliderRPC withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) { - if(![response.resultCode isEqualToEnum:SDLResultSuccess]) { + if (![response.resultCode isEqualToEnum:SDLResultSuccess]) { if ([response.resultCode isEqualToEnum:SDLResultTimedOut]) { [AlertManager sendAlertWithManager:manager image:nil textField1:AlertSliderTimedOutWarningText textField2:nil]; } else if ([response.resultCode isEqualToEnum:SDLResultAborted]) { @@ -151,7 +154,7 @@ NS_ASSUME_NONNULL_BEGIN return [[SDLMenuCell alloc] initWithTitle:ACScrollableMessageMenuName secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:@[ACScrollableMessageMenuName] handler:^(SDLTriggerSource _Nonnull triggerSource) { SDLScrollableMessage *messageRPC = [[SDLScrollableMessage alloc] initWithMessage:@"This is a scrollable message\nIt can contain many lines"]; [manager sendRequest:messageRPC withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) { - if(![response.resultCode isEqualToEnum:SDLResultSuccess]) { + if (![response.resultCode isEqualToEnum:SDLResultSuccess]) { if ([response.resultCode isEqualToEnum:SDLResultTimedOut]) { [AlertManager sendAlertWithManager:manager image:nil textField1:AlertScrollableMessageTimedOutWarningText textField2:nil]; } else if ([response.resultCode isEqualToEnum:SDLResultAborted]) { @@ -164,6 +167,50 @@ NS_ASSUME_NONNULL_BEGIN }]; } ++ (SDLMenuCell *)sdlex_menuCellRemoteWithManager:(SDLManager *)manager remoteManager:(RemoteControlManager *)remoteManager { + SDLArtwork *remoteControlIcon = [SDLArtwork artworkWithImage:[[UIImage imageNamed:RemoteControlIconName] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] asImageFormat:SDLArtworkImageFormatPNG]; + + // Clicking on cell shows alert message when remote control permissions are disabled + if (!remoteManager.isEnabled) { + return [[SDLMenuCell alloc] initWithTitle:ACRemoteMenuName secondaryText:nil tertiaryText:nil icon:remoteControlIcon secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) { + [AlertManager sendAlertWithManager:manager image:nil textField1:AlertRemoteControlNotEnabledWarningText textField2:nil]; + }]; + } + + // Let's give an example of 2 templates + NSMutableArray *submenuItems = [NSMutableArray array]; + NSString *errorMessage = @"Changing the template failed"; + + // Climate Control + SDLMenuCell *climateControlCell = [[SDLMenuCell alloc] initWithTitle:ACRemoteControlClimateMenuName secondaryText:nil tertiaryText:nil icon: nil secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) { + [manager.screenManager changeLayout:[[SDLTemplateConfiguration alloc] initWithPredefinedLayout:SDLPredefinedLayoutTilesOnly] withCompletionHandler:^(NSError * _Nullable error) { + if (error != nil) { + [AlertManager sendAlertWithManager:manager image:nil textField1:errorMessage textField2:nil]; + return; + } + [remoteManager showClimateControl]; + }]; + }]; + [submenuItems addObject:climateControlCell]; + + // View Climate + SDLMenuCell *viewClimateCell = [[SDLMenuCell alloc] initWithTitle:ACRemoteViewClimateMenuName secondaryText:nil tertiaryText:nil icon:nil secondaryArtwork:nil voiceCommands:nil handler:^(SDLTriggerSource _Nonnull triggerSource) { + SDLScrollableMessage *messageRPC = [[SDLScrollableMessage alloc] initWithMessage:remoteManager.climateDataString]; + [manager sendRequest:messageRPC withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) { + if ([response.resultCode isEqualToEnum:SDLResultTimedOut]) { + [AlertManager sendAlertWithManager:manager image:nil textField1:AlertScrollableMessageTimedOutWarningText textField2:nil]; + } else if ([response.resultCode isEqualToEnum:SDLResultAborted]) { + [AlertManager sendAlertWithManager:manager image:nil textField1:AlertScrollableMessageCancelledWarningText textField2:nil]; + } else if (![response.resultCode isEqualToEnum:SDLResultSuccess]) { + [AlertManager sendAlertWithManager:manager image:nil textField1:AlertScrollableMessageGeneralWarningText textField2:nil]; + } + }]; + }]; + [submenuItems addObject:viewClimateCell]; + + return [[SDLMenuCell alloc] initWithTitle:ACRemoteMenuName secondaryText:nil tertiaryText:nil icon:remoteControlIcon secondaryArtwork:nil submenuLayout:SDLMenuLayoutList subCells:[submenuItems copy]]; +} + #pragma mark - Voice Commands + (SDLVoiceCommand *)sdlex_voiceCommandStartWithManager:(SDLManager *)manager { diff --git a/Example Apps/Example ObjC/ProxyManager.m b/Example Apps/Example ObjC/ProxyManager.m index 35520d2f9..06f1d7334 100644 --- a/Example Apps/Example ObjC/ProxyManager.m +++ b/Example Apps/Example ObjC/ProxyManager.m @@ -9,6 +9,7 @@ #import "PerformInteractionManager.h" #import "Preferences.h" #import "ProxyManager.h" +#import "RemoteControlManager.h" #import "RPCPermissionsManager.h" #import "SmartDeviceLink.h" #import "SubscribeButtonManager.h" @@ -22,6 +23,8 @@ NS_ASSUME_NONNULL_BEGIN // Describes the first time the HMI state goes non-none and full. @property (assign, nonatomic) SDLHMILevel firstHMILevel; +@property (assign, nonatomic, getter=isRemoteControlEnabled) BOOL remoteControlEnabled; +@property (strong, nonatomic) RemoteControlManager *remoteControlManager; @property (strong, nonatomic) VehicleDataManager *vehicleDataManager; @property (strong, nonatomic) PerformInteractionManager *performManager; @property (strong, nonatomic) ButtonManager *buttonManager; @@ -50,6 +53,7 @@ NS_ASSUME_NONNULL_BEGIN return nil; } + _remoteControlEnabled = NO; _state = ProxyStateStopped; _firstHMILevel = SDLHMILevelNone; @@ -69,7 +73,7 @@ NS_ASSUME_NONNULL_BEGIN self.performManager = [[PerformInteractionManager alloc] initWithManager:self.sdlManager]; self.buttonManager = [[ButtonManager alloc] initWithManager:self.sdlManager refreshUIHandler:self.refreshUIHandler]; self.subscribeButtonManager = [[SubscribeButtonManager alloc] initWithManager:self.sdlManager]; - + self.remoteControlManager = [[RemoteControlManager alloc] initWithManager:self.sdlManager isEnabled:self.isRemoteControlEnabled softButtons:[self.buttonManager allScreenSoftButtons]]; [weakSelf sdlex_updateProxyState:ProxyStateConnected]; [RPCPermissionsManager setupPermissionsCallbacksWithManager:weakSelf.sdlManager]; @@ -99,18 +103,19 @@ NS_ASSUME_NONNULL_BEGIN [self sdlex_updateProxyState:ProxyStateSearchingForConnection]; SDLConfiguration *config = (proxyTransportType == ProxyTransportTypeIAP) ? [self.class sdlex_iapConfiguration] : [self.class sdlex_tcpConfiguration]; + self.remoteControlEnabled = (proxyTransportType == ProxyTransportTypeTCP); self.sdlManager = [[SDLManager alloc] initWithConfiguration:config delegate:self]; [self sdlex_startManager]; } + (SDLConfiguration *)sdlex_iapConfiguration { - SDLLifecycleConfiguration *lifecycleConfig = [self.class sdlex_setLifecycleConfigurationPropertiesOnConfiguration:[SDLLifecycleConfiguration defaultConfigurationWithAppName:ExampleAppName fullAppId:ExampleFullAppId]]; + SDLLifecycleConfiguration *lifecycleConfig = [self.class sdlex_setLifecycleConfigurationPropertiesOnConfiguration:[SDLLifecycleConfiguration defaultConfigurationWithAppName:ExampleAppName fullAppId:ExampleFullAppId] enableRemote:NO]; return [self sdlex_setupManagerConfigurationWithLifecycleConfiguration:lifecycleConfig]; } + (SDLConfiguration *)sdlex_tcpConfiguration { - SDLLifecycleConfiguration *lifecycleConfig = [self.class sdlex_setLifecycleConfigurationPropertiesOnConfiguration:[SDLLifecycleConfiguration debugConfigurationWithAppName:ExampleAppName fullAppId:ExampleFullAppId ipAddress:[Preferences sharedPreferences].ipAddress port:[Preferences sharedPreferences].port]]; + SDLLifecycleConfiguration *lifecycleConfig = [self.class sdlex_setLifecycleConfigurationPropertiesOnConfiguration:[SDLLifecycleConfiguration debugConfigurationWithAppName:ExampleAppName fullAppId:ExampleFullAppId ipAddress:[Preferences sharedPreferences].ipAddress port:[Preferences sharedPreferences].port] enableRemote:YES]; return [self sdlex_setupManagerConfigurationWithLifecycleConfiguration:lifecycleConfig]; } @@ -121,7 +126,7 @@ NS_ASSUME_NONNULL_BEGIN return [[SDLConfiguration alloc] initWithLifecycle:lifecycleConfiguration lockScreen:lockScreenConfiguration logging:[self.class sdlex_logConfiguration] fileManager:[SDLFileManagerConfiguration defaultConfiguration] encryption:[SDLEncryptionConfiguration defaultConfiguration]]; } -+ (SDLLifecycleConfiguration *)sdlex_setLifecycleConfigurationPropertiesOnConfiguration:(SDLLifecycleConfiguration *)config { ++ (SDLLifecycleConfiguration *)sdlex_setLifecycleConfigurationPropertiesOnConfiguration:(SDLLifecycleConfiguration *)config enableRemote:(BOOL)enableRemote { UIImage *appLogo = [[UIImage imageNamed:ExampleAppLogoName] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; SDLArtwork *appIconArt = [SDLArtwork persistentArtworkWithImage:appLogo asImageFormat:SDLArtworkImageFormatPNG]; @@ -133,6 +138,12 @@ NS_ASSUME_NONNULL_BEGIN config.languagesSupported = @[SDLLanguageEnUs, SDLLanguageFrCa, SDLLanguageEsMx]; config.appType = SDLAppHMITypeDefault; + // On actual hardware, the app requires permissions to do remote control which this example app will not have. + // Only use the remote control type on the TCP connection. + if (enableRemote) { + config.additionalAppTypes = @[SDLAppHMITypeRemoteControl]; + } + SDLRGBColor *green = [[SDLRGBColor alloc] initWithRed:126 green:188 blue:121]; SDLRGBColor *white = [[SDLRGBColor alloc] initWithRed:249 green:251 blue:254]; SDLRGBColor *darkGrey = [[SDLRGBColor alloc] initWithRed:57 green:78 blue:96]; @@ -145,7 +156,7 @@ NS_ASSUME_NONNULL_BEGIN + (SDLLogConfiguration *)sdlex_logConfiguration { SDLLogConfiguration *logConfig = [SDLLogConfiguration debugConfiguration]; - SDLLogFileModule *sdlExampleModule = [SDLLogFileModule moduleWithName:@"SDL Obj-C Example App" files:[NSSet setWithArray:@[@"ProxyManager", @"AlertManager", @"AudioManager", @"ButtonManager", @"SubscribeButtonManager", @"MenuManager", @"PerformInteractionManager", @"RPCPermissionsManager", @"VehicleDataManager"]]]; + SDLLogFileModule *sdlExampleModule = [SDLLogFileModule moduleWithName:@"SDL Obj-C Example App" files:[NSSet setWithArray:@[@"ProxyManager", @"AlertManager", @"AudioManager", @"ButtonManager", @"SubscribeButtonManager", @"MenuManager", @"PerformInteractionManager", @"RPCPermissionsManager", @"VehicleDataManager", @"RemoteControlManager"]]]; logConfig.modules = [logConfig.modules setByAddingObject:sdlExampleModule]; logConfig.targets = [logConfig.targets setByAddingObject:[SDLLogTargetFile logger]]; logConfig.globalLogLevel = SDLLogLevelDebug; @@ -156,7 +167,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Screen UI Helpers - (void)sdlex_createMenus { - self.sdlManager.screenManager.menu = [MenuManager allMenuItemsWithManager:self.sdlManager performManager:self.performManager]; + self.sdlManager.screenManager.menu = [MenuManager allMenuItemsWithManager:self.sdlManager performManager:self.performManager remoteManager:self.remoteControlManager]; self.sdlManager.screenManager.voiceCommands = [MenuManager allVoiceMenuItemsWithManager:self.sdlManager]; } @@ -242,10 +253,13 @@ NS_ASSUME_NONNULL_BEGIN if (![newLevel isEqualToEnum:SDLHMILevelNone] && ([self.firstHMILevel isEqualToEnum:SDLHMILevelNone])) { // This is our first time in a non-NONE state self.firstHMILevel = newLevel; - + // Subscribe to vehicle data. [self.vehicleDataManager subscribeToVehicleOdometer]; + // Start Remote Control Connection + [self.remoteControlManager start]; + //Handle initial launch [self sdlex_showInitialData]; } diff --git a/Example Apps/Example ObjC/RemoteControlManager.h b/Example Apps/Example ObjC/RemoteControlManager.h new file mode 100644 index 000000000..d85c49d5c --- /dev/null +++ b/Example Apps/Example ObjC/RemoteControlManager.h @@ -0,0 +1,29 @@ +// +// RemoteControlManager.h +// SmartDeviceLink-Example-ObjC +// +// Created by Beharry, Justin (J.S.) on 8/1/22. +// Copyright © 2022 smartdevicelink. All rights reserved. +// + +#import <Foundation/Foundation.h> + +@class SDLManager; +@class SDLSoftButtonObject; + +NS_ASSUME_NONNULL_BEGIN + +@interface RemoteControlManager : NSObject + +@property (assign, nonatomic, readonly, getter=isEnabled) BOOL enabled; +@property (copy, nonatomic, readonly) NSString *climateDataString; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithManager:(SDLManager *)manager isEnabled:(BOOL)enabled softButtons:(NSArray<SDLSoftButtonObject *> *)buttons; + +- (void)start; +- (void)showClimateControl; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Example Apps/Example ObjC/RemoteControlManager.m b/Example Apps/Example ObjC/RemoteControlManager.m new file mode 100644 index 000000000..05d74540b --- /dev/null +++ b/Example Apps/Example ObjC/RemoteControlManager.m @@ -0,0 +1,247 @@ +// +// RemoteControlManager.m +// SmartDeviceLink-Example-ObjC +// +// Created by Beharry, Justin (J.S.) on 8/1/22. +// Copyright © 2022 smartdevicelink. All rights reserved. +// + +#import "RemoteControlManager.h" + +#import "AlertManager.h" +#import "SmartDeviceLink.h" + +@interface RemoteControlManager() + +@property (strong, nonatomic) SDLManager *sdlManager; +@property (strong, nonatomic) NSArray<SDLSoftButtonObject *> *homeButtons; + +@property (strong, nonatomic) SDLRemoteControlCapabilities *remoteControlCapabilities; +@property (strong, nonatomic) NSString *climateModuleId; +@property (strong, nonatomic) NSNumber<SDLBool> *hasConsent; +@property (strong, nonatomic) SDLClimateControlData *climateData; +@property (copy, nonatomic, readwrite) NSString *climateDataString; +@property (strong, nonatomic, readonly) NSArray<SDLSoftButtonObject *> *remoteButtons; + +@end + +@implementation RemoteControlManager + +- (instancetype)initWithManager:(SDLManager *)manager isEnabled:(BOOL)enabled softButtons:(NSArray<SDLSoftButtonObject *> *)buttons { + self = [super init]; + if (!self) { return nil; } + + _sdlManager = manager; + _enabled = enabled; + _homeButtons = buttons; + + return self; +} + +- (void)start { + if (!self.isEnabled) { + SDLLogW(@"Missing permissions for Remote Control Manager. Example remote control works only on TCP."); + return; + } + + [self.sdlManager.systemCapabilityManager subscribeToCapabilityType:SDLSystemCapabilityTypeRemoteControl withUpdateHandler:^(SDLSystemCapability * _Nullable capability, BOOL subscribed, NSError * _Nullable error) { + if (!capability.remoteControlCapability) { + SDLLogE(@"SDL errored getting remote control module information: %@", error); + return; + } + + self.remoteControlCapabilities = capability.remoteControlCapability; + self.climateModuleId = self.remoteControlCapabilities.climateControlCapabilities.firstObject.moduleInfo.moduleId; + + // Get consent to control modules + SDLGetInteriorVehicleDataConsent *getInteriorVehicleDataConsent = [[SDLGetInteriorVehicleDataConsent alloc] initWithModuleType:SDLModuleTypeClimate moduleIds:@[self.climateModuleId]]; + [self.sdlManager sendRequest:getInteriorVehicleDataConsent withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) { + if (!response.success) { + SDLLogE(@"SDL errored getting remote control consent: %@", error); + return; + } + self.hasConsent = response.success; + + // Initialize climate data and setup subscription + if (self.hasConsent) { + [self sdlex_initializeClimateData]; + [self sdlex_subscribeClimateControlData]; + } + }]; + }]; +} + +- (void)showClimateControl { + if (self.climateModuleId == nil && self.hasConsent) { + NSString *errorMessage = @"The climate module id was not set or consent was not given"; + [AlertManager sendAlertWithManager:self.sdlManager image:nil textField1:errorMessage textField2:nil]; + return; + } + + // Set the soft buttons to change remote control parameters + self.sdlManager.screenManager.softButtonObjects = self.remoteButtons; +} + +- (NSString *)climateDataString { + return [NSString stringWithFormat:@"AC: %@\n" + "AC Max: %@\n" + "Auto Mode: %@\n" + "Circulate Air: %@\n" + "Climate: %@\n" + "Current Temperature: %@\n" + "Defrost Zone: %@\n" + "Desired Temperature: %@\n" + "Dual Mode: %@\n" + "Fan Speed: %@\n" + "Heated Mirrors: %@\n" + "Heated Rears Window: %@\n" + "Heated Steering: %@\n" + "Heated Windshield: %@\n" + "Ventilation: %@\n", + self.climateData.acEnable.boolValue ? @"On" : @"Off", + self.climateData.acMaxEnable.boolValue ? @"On" : @"Off", + self.climateData.autoModeEnable.boolValue ? @"On" : @"Off", + self.climateData.circulateAirEnable.boolValue ? @"On" : @"Off", + self.climateData.climateEnable.boolValue ? @"On" : @"Off", + self.climateData.currentTemperature, + self.climateData.defrostZone, + self.climateData.desiredTemperature, + self.climateData.dualModeEnable.boolValue ? @"On" : @"Off", + self.climateData.fanSpeed, + self.climateData.heatedMirrorsEnable.boolValue ? @"On" : @"Off", + self.climateData.heatedRearWindowEnable.boolValue ? @"On" : @"Off", + self.climateData.heatedSteeringWheelEnable.boolValue ? @"On" : @"Off", + self.climateData.heatedWindshieldEnable.boolValue ? @"On" : @"Off", + self.climateData.ventilationMode + ]; +} + +- (void)sdlex_initializeClimateData { + if (self.climateModuleId == nil && !self.hasConsent.boolValue) { + NSString *errorMessage = @"The climate module id was not set or consent was not given"; + [AlertManager sendAlertWithManager:self.sdlManager image:nil textField1:errorMessage textField2:nil]; + } + + SDLGetInteriorVehicleData *getInteriorVehicleData = [[SDLGetInteriorVehicleData alloc] initWithModuleType:SDLModuleTypeClimate moduleId:self.climateModuleId]; + [self.sdlManager sendRequest:getInteriorVehicleData withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) { + SDLGetInteriorVehicleDataResponse *dataResponse = (SDLGetInteriorVehicleDataResponse *)response; + self.climateData = dataResponse.moduleData.climateControlData; + }]; +} + +- (void)sdlex_subscribeClimateControlData { + // Start the subscription to remote control data + [self.sdlManager subscribeToRPC:SDLDidReceiveInteriorVehicleDataNotification withBlock:^(__kindof SDLRPCMessage * _Nonnull message) { + SDLOnInteriorVehicleData *onInteriorVehicleData = (SDLOnInteriorVehicleData *)message; + self.climateData = onInteriorVehicleData.moduleData.climateControlData; + }]; + + // Start the subscriptin to climate data + SDLGetInteriorVehicleData *getInteriorVehicleData = [[SDLGetInteriorVehicleData alloc] initAndSubscribeToModuleType:SDLModuleTypeClimate moduleId:self.climateModuleId]; + [self.sdlManager sendRequest:getInteriorVehicleData withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) { + if (error != nil) { + return SDLLogE(@"SDL errored trying to subscribe to climate data: %@", error); + } + SDLLogD(@"SDL Subscribing to climate control data"); + }]; +} + +- (void)sdlex_turnOnAC { + SDLClimateControlData *climateControlData = [[SDLClimateControlData alloc] initWithDictionary:@{ @"acEnable": @YES }]; + SDLModuleData *moduleData = [[SDLModuleData alloc] initWithClimateControlData:climateControlData]; + SDLSetInteriorVehicleData *setInteriorVehicleData = [[SDLSetInteriorVehicleData alloc] initWithModuleData:moduleData]; + + [self.sdlManager sendRequest:setInteriorVehicleData withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) { + if (!response.success) { + SDLLogE(@"SDL errored trying to turn on climate AC: %@", error); + return; + } + }]; +} + +- (void)sdlex_turnOffAC { + SDLClimateControlData *climateControlData = [[SDLClimateControlData alloc] initWithDictionary:@{ @"acEnable": @NO }]; + SDLModuleData *moduleData = [[SDLModuleData alloc] initWithClimateControlData:climateControlData]; + SDLSetInteriorVehicleData *setInteriorVehicleData = [[SDLSetInteriorVehicleData alloc] initWithModuleData:moduleData]; + + [self.sdlManager sendRequest:setInteriorVehicleData withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) { + if (!response.success) { + SDLLogE(@"SDL errored trying to turn off climate AC: %@", error); + return; + } + }]; +} + +- (void)sdlex_setClimateTemperature { + SDLTemperature *temperature = [[SDLTemperature alloc] initWithFahrenheitValue:73]; + NSDictionary<NSString *, id> *climateDictionary = @{@"acEnable": @YES, @"fanSpeed": @100, @"desiredTemperature": temperature, @"ventilationMode": SDLVentilationModeBoth }; + + SDLClimateControlData *climateControlData = [[SDLClimateControlData alloc] initWithDictionary:climateDictionary]; + SDLModuleData *moduleData = [[SDLModuleData alloc] initWithClimateControlData:climateControlData]; + SDLSetInteriorVehicleData *setInteriorVehicleData = [[SDLSetInteriorVehicleData alloc] initWithModuleData:moduleData]; + + [self.sdlManager sendRequest:setInteriorVehicleData withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) { + if (!response.success) { + SDLLogE(@"SDL errored trying to set climate temperature to 73 degrees: %@", error); + return; + } + }]; +} + +- (NSArray *)remoteButtons { + SDLSoftButtonObject *acOnButton = [[SDLSoftButtonObject alloc] initWithName:@"AC On" text:@"AC On" artwork:nil handler:^(SDLOnButtonPress * _Nullable buttonPress, SDLOnButtonEvent * _Nullable buttonEvent) { + if (buttonPress == nil) { return; } + [self sdlex_turnOnAC]; + }]; + + SDLSoftButtonObject *acOffButton = [[SDLSoftButtonObject alloc] initWithName:@"AC Off" text:@"AC Off" artwork:nil handler:^(SDLOnButtonPress * _Nullable buttonPress, SDLOnButtonEvent * _Nullable buttonEvent) { + if (buttonPress == nil) { return; } + [self sdlex_turnOffAC]; + }]; + + SDLSoftButtonObject *acMaxToggle = [[SDLSoftButtonObject alloc] initWithName:@"AC Max" text:@"AC Max" artwork:nil handler:^(SDLOnButtonPress * _Nullable buttonPress, SDLOnButtonEvent * _Nullable buttonEvent) { + if (buttonPress == nil) { return; } + + SDLButtonPress *buttonTouch = [[SDLButtonPress alloc] initWithButtonName:SDLButtonNameACMax moduleType:SDLModuleTypeClimate moduleId:self.climateModuleId buttonPressMode:SDLButtonPressModeShort]; + [self.sdlManager sendRequest:buttonTouch withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) { + if (!response.success) { + SDLLogE(@"SDL errored toggle AC Max with remote button press: %@", error); + return; + } + }]; + }]; + + SDLSoftButtonObject *temperatureDecreaseButton = [[SDLSoftButtonObject alloc] initWithName:@"Temperature Decrease" text:@"Temperature -" artwork:nil handler:^(SDLOnButtonPress * _Nullable buttonPress, SDLOnButtonEvent * _Nullable buttonEvent) { + if (buttonPress == nil) { return; } + + SDLButtonPress *buttonTouch = [[SDLButtonPress alloc] initWithButtonName:SDLButtonNameTempDown moduleType:SDLModuleTypeClimate moduleId:self.climateModuleId buttonPressMode:SDLButtonPressModeShort]; + [self.sdlManager sendRequest:buttonTouch withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) { + if (!response.success) { + SDLLogE(@"SDL errored decreasing target climate temperature with remote button: %@", error); + return; + } + }]; + }]; + + SDLSoftButtonObject *temperatureIncreaseButton = [[SDLSoftButtonObject alloc] initWithName:@"Temperature Increase" text:@"Temperature +" artwork:nil handler:^(SDLOnButtonPress * _Nullable buttonPress, SDLOnButtonEvent * _Nullable buttonEvent) { + if (buttonPress == nil) { return; } + + SDLButtonPress *buttonTouch = [[SDLButtonPress alloc] initWithButtonName:SDLButtonNameTempUp moduleType:SDLModuleTypeClimate moduleId:self.climateModuleId buttonPressMode:SDLButtonPressModeShort]; + [self.sdlManager sendRequest:buttonTouch withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) { + if (!response.success) { + SDLLogE(@"SDL errored increasing target climate temperature with remote button:: %@", error); + return; + } + }]; + }]; + + SDLSoftButtonObject *backToHomeButton = [[SDLSoftButtonObject alloc] initWithName:@"Home" text:@"Back to Home" artwork:nil handler:^(SDLOnButtonPress * _Nullable buttonPress, SDLOnButtonEvent * _Nullable buttonEvent) { + if (buttonPress == nil) { return; } + self.sdlManager.screenManager.softButtonObjects = self.homeButtons; + [self.sdlManager.screenManager changeLayout:[[SDLTemplateConfiguration alloc] initWithPredefinedLayout:SDLPredefinedLayoutNonMedia] withCompletionHandler:nil]; + }]; + + return @[acOnButton, acOffButton, acMaxToggle, temperatureDecreaseButton, temperatureIncreaseButton, backToHomeButton]; +} + +@end diff --git a/Example Apps/Example Swift/MenuManager.swift b/Example Apps/Example Swift/MenuManager.swift index 092670559..b35af48aa 100644 --- a/Example Apps/Example Swift/MenuManager.swift +++ b/Example Apps/Example Swift/MenuManager.swift @@ -15,9 +15,10 @@ class MenuManager: NSObject { /// /// - Parameter manager: The SDL Manager /// - Returns: An array of SDLAddCommand objects - class func allMenuItems(with manager: SDLManager, choiceSetManager: PerformInteractionManager) -> [SDLMenuCell] { + class func allMenuItems(with manager: SDLManager, choiceSetManager: PerformInteractionManager, remoteManager: RemoteControlManager) -> [SDLMenuCell] { return [menuCellSpeakName(with: manager), menuCellGetAllVehicleData(with: manager), + menuCellRemoteControl(with: manager, remoteManager: remoteManager), menuCellShowPerformInteraction(with: manager, choiceSetManager: choiceSetManager), sliderMenuCell(with: manager), scrollableMessageMenuCell(with: manager), @@ -117,11 +118,11 @@ private extension MenuManager { /// - Returns: A SDLMenuCell object class func menuCellChangeTemplate(with manager: SDLManager) -> SDLMenuCell { - /// Lets give an example of 2 templates + // Lets give an example of 2 templates var submenuItems = [SDLMenuCell]() let errorMessage = "Changing the template failed" - /// Non-Media + // Non-Media let submenuTitleNonMedia = "Non - Media (Default)" submenuItems.append(SDLMenuCell(title: submenuTitleNonMedia, secondaryText: nil, tertiaryText: nil, icon: nil, secondaryArtwork: nil, voiceCommands: nil, handler: { (triggerSource) in manager.screenManager.changeLayout(SDLTemplateConfiguration(predefinedLayout: .nonMedia)) { err in @@ -132,7 +133,7 @@ private extension MenuManager { } })) - /// Graphic with Text + // Graphic with Text let submenuTitleGraphicText = "Graphic With Text" submenuItems.append(SDLMenuCell(title: submenuTitleGraphicText, secondaryText: nil, tertiaryText: nil, icon: nil, secondaryArtwork: nil, voiceCommands: nil, handler: { (triggerSource) in manager.screenManager.changeLayout(SDLTemplateConfiguration(predefinedLayout: .graphicWithText)) { err in @@ -206,6 +207,55 @@ private extension MenuManager { }) }) } + + /// Menu item that shows remote control example + /// + /// - Parameters: + /// - manager: The SDL Manager + /// - remoteManager: The manager for controling and viewing remote control data + /// - Returns: A SDLMenuCell object + class func menuCellRemoteControl(with manager: SDLManager, remoteManager: RemoteControlManager) -> SDLMenuCell { + let remoteControlIcon = SDLArtwork(image: UIImage(named: RemoteControlIconName)!.withRenderingMode(.alwaysTemplate), persistent: true, as: .PNG) + + // Clicking on cell shows alert message when remote control permissions are disabled + guard remoteManager.isEnabled else { + return SDLMenuCell(title: ACRemoteMenuName, secondaryText: nil, tertiaryText: nil, icon: remoteControlIcon, secondaryArtwork: nil, voiceCommands: nil, handler: { _ in + AlertManager.sendAlert(textField1: AlertRemoteControlNotEnabledWarningText, sdlManager: manager) + }) + } + + var submenuItems = [SDLMenuCell]() + // Climate Control Menu + submenuItems.append(SDLMenuCell(title: ACRemoteControlClimateMenuName, secondaryText: nil, tertiaryText: nil, icon: nil, secondaryArtwork: nil, voiceCommands: nil, handler: { (triggerSource) in + manager.screenManager.changeLayout(SDLTemplateConfiguration(predefinedLayout: .tilesOnly)) { err in + if let error = err { + AlertManager.sendAlert(textField1: error.localizedDescription, sdlManager: manager) + return + } + remoteManager.showClimateControl() + } + })) + + // View Climate Data + submenuItems.append(SDLMenuCell(title: ACRemoteViewClimateMenuName, secondaryText: nil, tertiaryText: nil, icon: nil, secondaryArtwork: nil, voiceCommands: nil, handler: { _ in + let climateDataMessage = SDLScrollableMessage(message: remoteManager.climateDataString) + manager.send(request: climateDataMessage, responseHandler: { (request, response, error) in + guard let response = response else { return } + guard response.resultCode == .success else { + if response.resultCode == .timedOut { + AlertManager.sendAlert(textField1: AlertScrollableMessageTimedOutWarningText, sdlManager: manager) + } else if response.resultCode == .aborted { + AlertManager.sendAlert(textField1: AlertScrollableMessageCancelledWarningText, sdlManager: manager) + } else { + AlertManager.sendAlert(textField1: AlertScrollableMessageGeneralWarningText, sdlManager: manager) + } + return + } + }) + })) + + return SDLMenuCell(title: ACRemoteMenuName, secondaryText: nil, tertiaryText: nil, icon: remoteControlIcon, secondaryArtwork: nil, submenuLayout: .list, subCells: submenuItems) + } } // MARK: - Menu Voice Commands diff --git a/Example Apps/Example Swift/ProxyManager.swift b/Example Apps/Example Swift/ProxyManager.swift index efd364784..9a9d10140 100644 --- a/Example Apps/Example Swift/ProxyManager.swift +++ b/Example Apps/Example Swift/ProxyManager.swift @@ -26,7 +26,9 @@ class ProxyManager: NSObject { private var subscribeButtonManager: SubscribeButtonManager! private var vehicleDataManager: VehicleDataManager! private var performInteractionManager: PerformInteractionManager! + private var remoteControlManager: RemoteControlManager! private var firstHMILevelState: SDLHMILevel + private var isRemoteControlEnabled: Bool weak var delegate: ProxyManagerDelegate? @@ -34,6 +36,7 @@ class ProxyManager: NSObject { static let sharedManager = ProxyManager() private override init() { firstHMILevelState = .none + isRemoteControlEnabled = false; super.init() } } @@ -48,6 +51,7 @@ extension ProxyManager { delegate?.didChangeProxyState(ProxyState.searching) sdlManager = SDLManager(configuration: (proxyTransportType == .iap) ? ProxyManager.iapConfiguration : ProxyManager.tcpConfiguration, delegate: self) + self.isRemoteControlEnabled = (proxyTransportType == .tcp) startManager() } @@ -74,7 +78,7 @@ private extension ProxyManager { /// - Returns: A SDLConfiguration object class var iapConfiguration: SDLConfiguration { let lifecycleConfiguration = SDLLifecycleConfiguration(appName: ExampleAppName, fullAppId: ExampleFullAppId) - return setupManagerConfiguration(with: lifecycleConfiguration) + return setupManagerConfiguration(with: lifecycleConfiguration, enableRemote: false) } /// Configures a TCP transport layer with the IP address and port of the remote SDL Core instance. @@ -82,18 +86,25 @@ private extension ProxyManager { /// - Returns: A SDLConfiguration object class var tcpConfiguration: SDLConfiguration { let lifecycleConfiguration = SDLLifecycleConfiguration(appName: ExampleAppName, fullAppId: ExampleFullAppId, ipAddress: AppUserDefaults.shared.ipAddress!, port: UInt16(AppUserDefaults.shared.port!)!) - return setupManagerConfiguration(with: lifecycleConfiguration) + return setupManagerConfiguration(with: lifecycleConfiguration, enableRemote: true) } /// Helper method for setting additional configuration parameters for both TCP and iAP transport layers. /// /// - Parameter lifecycleConfiguration: The transport layer configuration /// - Returns: A SDLConfiguration object - class func setupManagerConfiguration(with lifecycleConfiguration: SDLLifecycleConfiguration) -> SDLConfiguration { + class func setupManagerConfiguration(with lifecycleConfiguration: SDLLifecycleConfiguration, enableRemote: Bool) -> SDLConfiguration { lifecycleConfiguration.shortAppName = ExampleAppNameShort let appIcon = UIImage(named: ExampleAppLogoName)?.withRenderingMode(.alwaysOriginal) lifecycleConfiguration.appIcon = appIcon != nil ? SDLArtwork(image: appIcon!, persistent: true, as: .PNG) : nil lifecycleConfiguration.appType = .default + + // On actual hardware, the app requires permissions to do remote control which this example app will not have. + // Only use the remote control type on the TCP connection. + if enableRemote { + lifecycleConfiguration.additionalAppTypes = [.remoteControl] + } + lifecycleConfiguration.language = .enUs lifecycleConfiguration.languagesSupported = [.enUs, .esMx, .frCa] lifecycleConfiguration.ttsName = [SDLTTSChunk(text: "S D L", type: .text)] @@ -114,7 +125,7 @@ private extension ProxyManager { /// - Returns: A SDLLogConfiguration object class func logConfiguration() -> SDLLogConfiguration { let logConfig = SDLLogConfiguration.default() - let exampleLogFileModule = SDLLogFileModule(name: "SDL Swift Example App", files: ["ProxyManager", "AlertManager", "AudioManager", "ButtonManager", "SubscribeButtonManager", "MenuManager", "PerformInteractionManager", "RPCPermissionsManager", "VehicleDataManager"]) + let exampleLogFileModule = SDLLogFileModule(name: "SDL Swift Example App", files: ["ProxyManager", "AlertManager", "AudioManager", "ButtonManager", "SubscribeButtonManager", "MenuManager", "PerformInteractionManager", "RPCPermissionsManager", "VehicleDataManager", "RemoteControlManager"]) logConfig.modules.insert(exampleLogFileModule) _ = logConfig.targets.insert(SDLLogTargetFile()) // Logs to file logConfig.globalLogLevel = .debug // Filters the logs @@ -136,6 +147,7 @@ private extension ProxyManager { self.subscribeButtonManager = SubscribeButtonManager(sdlManager: self.sdlManager) self.vehicleDataManager = VehicleDataManager(sdlManager: self.sdlManager, refreshUIHandler: self.refreshUIHandler) self.performInteractionManager = PerformInteractionManager(sdlManager: self.sdlManager) + self.remoteControlManager = RemoteControlManager(sdlManager: self.sdlManager, enabled: self.isRemoteControlEnabled, homeButtons: self.buttonManager.allScreenSoftButtons()) RPCPermissionsManager.setupPermissionsCallbacks(with: self.sdlManager) @@ -169,6 +181,9 @@ extension ProxyManager: SDLManagerDelegate { // Subscribe to vehicle data. vehicleDataManager.subscribeToVehicleOdometer() + // Start Remote Control Connection + remoteControlManager.start() + //Handle initial launch showInitialData() } @@ -306,7 +321,7 @@ private extension ProxyManager { func createMenuAndGlobalVoiceCommands() { // Send the root menu items let screenManager = sdlManager.screenManager - let menuItems = MenuManager.allMenuItems(with: sdlManager, choiceSetManager: performInteractionManager) + let menuItems = MenuManager.allMenuItems(with: sdlManager, choiceSetManager: performInteractionManager, remoteManager: remoteControlManager) let voiceMenuItems = MenuManager.allVoiceMenuItems(with: sdlManager) if !menuItems.isEmpty { screenManager.menu = menuItems } diff --git a/Example Apps/Example Swift/RemoteControlManager.swift b/Example Apps/Example Swift/RemoteControlManager.swift new file mode 100644 index 000000000..fc73434b4 --- /dev/null +++ b/Example Apps/Example Swift/RemoteControlManager.swift @@ -0,0 +1,226 @@ +// +// RemoteControlManager.swift +// SmartDeviceLink-Example-Swift +// +// Created by Beharry, Justin (J.S.) on 7/28/22. +// Copyright © 2022 smartdevicelink. All rights reserved. +// + +import Foundation +import SmartDeviceLink +import SmartDeviceLinkSwift + +class RemoteControlManager { + private let sdlManager: SDLManager + private let homeButtons: [SDLSoftButtonObject] + private var remoteControlCapabilities: SDLRemoteControlCapabilities? + private var climateModuleId: String? + private var hasConsent: Bool? + private var climateData: SDLClimateControlData? + + let isEnabled: Bool + var climateDataString: String { + """ + AC: \(optionalNumberBoolToString(climateData?.acEnable)) + AC Max: \(optionalNumberBoolToString(climateData?.acMaxEnable)) + Auto Mode: \(optionalNumberBoolToString(climateData?.autoModeEnable)) + Circulate Air: \(optionalNumberBoolToString(climateData?.circulateAirEnable)) + Climate: \(optionalNumberBoolToString(climateData?.climateEnable)) + Current Temperature: \(optionalTemperatureToString(climateData?.currentTemperature)) + Defrost Zone: \(optionalSDLEnumToString(climateData?.defrostZone?.rawValue)) + Desired Temperature: \(optionalTemperatureToString(climateData?.desiredTemperature)) + Dual Mode: \(optionalNumberBoolToString(climateData?.dualModeEnable)) + Fan Speed: \(optionalNumberToString(climateData?.fanSpeed)) + Heated Mirrors: \(optionalNumberBoolToString(climateData?.heatedMirrorsEnable)) + Heated Rears Window: \(optionalNumberBoolToString(climateData?.heatedRearWindowEnable)) + Heated Steering: \(optionalNumberBoolToString(climateData?.heatedSteeringWheelEnable)) + Heated Windshield: \(optionalNumberBoolToString(climateData?.heatedWindshieldEnable)) + Ventilation: \(optionalSDLEnumToString(climateData?.ventilationMode?.rawValue)) + """ + } + + /// Initialize the RemoteControlManager Object + /// + /// - Parameters: + /// - sdlManager: The SDL Manager. + /// - enabled: Permission from the proxy manager to access remote control data + /// - homeButton: An array of SDLSoftButtonObjects that remote control manager can reset to. + init(sdlManager: SDLManager, enabled: Bool, homeButtons: [SDLSoftButtonObject]) { + self.sdlManager = sdlManager + self.isEnabled = enabled + self.homeButtons = homeButtons + } + + func start() { + if !self.isEnabled { + return SDLLog.w("Missing permissions for Remote Control Manager. Example remote control works only on TCP.") + } + + // Retrieve remote control information and store module ids + self.sdlManager.systemCapabilityManager.subscribe(capabilityType: .remoteControl) { (capability, subscribed, error) in + guard capability?.remoteControlCapability != nil else { + return SDLLog.e("SDL errored getting remote control module information: \(String(describing: error))") + } + self.remoteControlCapabilities = capability?.remoteControlCapability + + let firstClimateModule = self.remoteControlCapabilities?.climateControlCapabilities?.first + let moduleId = firstClimateModule?.moduleInfo?.moduleId + self.climateModuleId = moduleId + + // Get Consent to control module + let getInteriorVehicleDataConsent = SDLGetInteriorVehicleDataConsent(moduleType: .climate, moduleIds: [self.climateModuleId!]) + self.sdlManager.send(request: getInteriorVehicleDataConsent, responseHandler: { (request, response, error) in + guard let response = response as? SDLGetInteriorVehicleDataConsentResponse else { + return SDLLog.e("SDL errored getting remote control consent: \(String(describing: error))"); + } + guard let allowed = response.allowed?.first?.boolValue else { return } + + self.hasConsent = allowed + + // initialize climate data and setup subscription + if self.hasConsent == true { + self.initializeClimateData() + self.subscribeClimateControlData() + } + }) + } + } + + /// Displays Buttons for the user to control the climate + func showClimateControl() { + // Check that the climate module id has been set and consent has been given + guard climateModuleId != nil && hasConsent == true else { + return AlertManager.sendAlert(textField1: "The climate module id was not set or consent was not given", sdlManager: self.sdlManager) + } + self.sdlManager.screenManager.softButtonObjects = climateButtons + } + + private func optionalNumberBoolToString(_ number: NSNumber?) -> String { + guard let number = number else { return "Unknown" } + return number.boolValue ? "On" : "Off" + } + + private func optionalNumberToString(_ number: NSNumber?) -> String { + guard let number = number else { return "Unknown" } + return number.stringValue + } + + private func optionalTemperatureToString(_ temperature: SDLTemperature?) -> String { + guard let temperature = temperature else { return "Unknown" } + return temperature.description + } + + private func optionalSDLEnumToString(_ sdlEnum: SDLEnum?) -> String { + guard let sdlEnum = sdlEnum else { return "Unknown" } + return sdlEnum.rawValue + } + + private func initializeClimateData() { + // Check that the climate module id has been set and consent has been given + guard climateModuleId != nil && hasConsent == true else { + return AlertManager.sendAlert(textField1: "The climate module id was not set or consent was not given", sdlManager: self.sdlManager) + } + + let getInteriorVehicleData = SDLGetInteriorVehicleData(moduleType: .climate, moduleId: self.climateModuleId!) + self.sdlManager.send(request: getInteriorVehicleData) { (req, res, err) in + guard let response = res as? SDLGetInteriorVehicleDataResponse else { return } + self.climateData = response.moduleData?.climateControlData + } + } + + private func subscribeClimateControlData() { + // Start the subscription to remote control data + sdlManager.subscribe(to: .SDLDidReceiveInteriorVehicleData) { (message) in + guard let onInteriorVehicleData = message as? SDLOnInteriorVehicleData else { return } + self.climateData = onInteriorVehicleData.moduleData.climateControlData + } + + // Start the subscription to climate data + let getInteriorVehicleData = SDLGetInteriorVehicleData(andSubscribeToModuleType: .climate, moduleId: self.climateModuleId!) + sdlManager.send(request: getInteriorVehicleData) { (req, res, err) in + guard let response = res as? SDLGetInteriorVehicleDataResponse, response.success.boolValue == true else { + return SDLLog.e("SDL errored trying to subscribe to climate data: \(String(describing: err))") + } + SDLLog.d("Subscribed to climate control data"); + } + } + + private func turnOnAC() { + let climateControlData = SDLClimateControlData(dictionary: [ + "acEnable": true + ]) + let moduleData = SDLModuleData(climateControlData: climateControlData) + let setInteriorVehicleData = SDLSetInteriorVehicleData(moduleData: moduleData) + + self.sdlManager.send(request: setInteriorVehicleData) { (request, response, error) in + guard response?.success.boolValue == true else { + return SDLLog.e("SDL errored trying to turn on climate AC: \(String(describing: error))") + } + } + } + + private func turnOffAC() { + let climateControlData = SDLClimateControlData(dictionary: [ + "acEnable": false + ]) + let moduleData = SDLModuleData(climateControlData: climateControlData) + let setInteriorVehicleData = SDLSetInteriorVehicleData(moduleData: moduleData) + + self.sdlManager.send(request: setInteriorVehicleData) { (request, response, error) in + guard response?.success.boolValue == true else { + return SDLLog.e("SDL errored trying to turn off climate AC: \(String(describing: error))") + } + } + } + + // An array of button objects to control the climate + private var climateButtons : [SDLSoftButtonObject] { + let acOnButton = SDLSoftButtonObject(name: "AC On", text: "AC On", artwork: nil) { (buttonPress, buttonEvent) in + guard buttonPress != nil else { return } + self.turnOnAC() + } + + let acOffButton = SDLSoftButtonObject(name: "AC Off", text: "AC Off", artwork: nil) { (buttonPress, buttonEvent) in + guard buttonPress != nil else { return } + self.turnOffAC() + } + + let acMaxToggle = SDLSoftButtonObject(name: "AC Max", text: "AC Max", artwork: nil) { (buttonPress, buttonEvent) in + guard buttonPress != nil else { return } + let remoteButtonPress = SDLButtonPress(buttonName: .acMax, moduleType: .climate, moduleId: self.climateModuleId, buttonPressMode: .short) + self.sdlManager.send(request: remoteButtonPress) { (request, response, error) in + guard response?.success.boolValue == true else { + return SDLLog.e("SDL errored toggling AC Max with remote button press: \(String(describing: error))") + } + } + } + + let temperatureDecreaseButton = SDLSoftButtonObject(name: "Temperature Decrease", text: "Temperature -", artwork: nil) { (buttonPress, buttonEvent) in + guard buttonPress != nil else { return } + let remoteButtonPress = SDLButtonPress(buttonName: .tempDown, moduleType: .climate, moduleId: self.climateModuleId, buttonPressMode: .short) + self.sdlManager.send(request: remoteButtonPress) { (request, response, error) in + guard response?.success.boolValue == true else { + return SDLLog.e("SDL errored decreasing target climate temperature with remote button: \(String(describing: error))") + } + } + } + + let temperatureIncreaseButton = SDLSoftButtonObject(name: "Temperature Increase", text: "Temperature +", artwork: nil) { (buttonPress, buttonEvent) in + guard buttonPress != nil else { return } + let remoteButtonPress = SDLButtonPress(buttonName: .tempUp, moduleType: .climate, moduleId: self.climateModuleId, buttonPressMode: .short) + self.sdlManager.send(request: remoteButtonPress) { (request, response, error) in + guard response?.success.boolValue == true else { + return SDLLog.e("SDL errored increasing target climate temperature with remote button:: \(String(describing: error))") + } + } + } + + let backToHomeButton = SDLSoftButtonObject(name: "Home", text: "Back to Home", artwork: nil) { (buttonPress, buttonEvent) in + guard buttonPress != nil else { return } + self.sdlManager.screenManager.softButtonObjects = self.homeButtons + self.sdlManager.screenManager.changeLayout(SDLTemplateConfiguration(predefinedLayout: .nonMedia)) + } + + return [acOnButton, acOffButton, acMaxToggle, temperatureDecreaseButton, temperatureIncreaseButton, backToHomeButton] + } +} diff --git a/Example Apps/Shared/AppConstants.h b/Example Apps/Shared/AppConstants.h index 36f25b4b7..94d698e0c 100644 --- a/Example Apps/Shared/AppConstants.h +++ b/Example Apps/Shared/AppConstants.h @@ -59,6 +59,7 @@ extern NSString * const AlertScrollableMessageCancelledWarningText; extern NSString * const AlertScrollableMessageGeneralWarningText; extern NSString * const AlertVehicleDataPermissionsWarningText; extern NSString * const AlertVehicleDataGeneralWarningText; +extern NSString * const AlertRemoteControlNotEnabledWarningText; extern NSString * const AlertSpeechPermissionsWarningText; #pragma mark - SDL Text-To-Speech @@ -119,6 +120,9 @@ extern NSString * const ACMyKeyMenuName; extern NSString * const ACOdometerMenuName; extern NSString * const ACPRNDLMenuName; extern NSString * const ACRPMMenuName; +extern NSString * const ACRemoteMenuName; +extern NSString * const ACRemoteControlClimateMenuName; +extern NSString * const ACRemoteViewClimateMenuName; extern NSString * const ACSpeedMenuName; extern NSString * const ACSteeringWheelAngleMenuName; extern NSString * const ACTirePressureMenuName; @@ -136,6 +140,7 @@ extern NSString * const PhoneBWIconImageName; extern NSString * const SpeakBWIconImageName; extern NSString * const BatteryEmptyBWIconName; extern NSString * const BatteryFullBWIconName; +extern NSString * const RemoteControlIconName; #pragma mark - SDL App Name in Different Languages extern NSString * const ExampleAppNameSpanish; diff --git a/Example Apps/Shared/AppConstants.m b/Example Apps/Shared/AppConstants.m index 20afa0b6c..1524547bf 100644 --- a/Example Apps/Shared/AppConstants.m +++ b/Example Apps/Shared/AppConstants.m @@ -56,6 +56,7 @@ NSString * const AlertScrollableMessageCancelledWarningText = @"Scrollable Messa NSString * const AlertScrollableMessageGeneralWarningText = @"Scrollable Message could not be displayed"; NSString * const AlertVehicleDataPermissionsWarningText = @"This app does not have the required permissions to access vehicle data"; NSString * const AlertVehicleDataGeneralWarningText = @"Something went wrong while getting vehicle data"; +NSString * const AlertRemoteControlNotEnabledWarningText = @"Remote Control is disabled on hardware. Connect this app to Manticore to test Remote Control."; NSString * const AlertSpeechPermissionsWarningText = @"You must give this app permission to access Speech Recognition"; #pragma mark - SDL Text-To-Speech @@ -116,6 +117,9 @@ NSString * const ACMyKeyMenuName = @"MyKey"; NSString * const ACOdometerMenuName = @"Odometer"; NSString * const ACPRNDLMenuName = @"PRNDL"; NSString * const ACRPMMenuName = @"RPM"; +NSString * const ACRemoteMenuName = @"Remote Control"; +NSString * const ACRemoteControlClimateMenuName = @"Climate Control"; +NSString * const ACRemoteViewClimateMenuName = @"View Climate"; NSString * const ACSpeedMenuName = @"Speed"; NSString * const ACSteeringWheelAngleMenuName = @"Steering Wheel Angle"; NSString * const ACTirePressureMenuName = @"Tire Pressure"; @@ -133,6 +137,7 @@ NSString * const PhoneBWIconImageName = @"phone"; NSString * const SpeakBWIconImageName = @"speak"; NSString * const BatteryEmptyBWIconName = @"toggle_off"; NSString * const BatteryFullBWIconName = @"toggle_on"; +NSString * const RemoteControlIconName = @"remote_control"; #pragma mark - SDL App Name in Different Languages NSString * const ExampleAppNameSpanish = @"SDL Aplicación de ejemplo"; diff --git a/Example Apps/Shared/Images.xcassets/remote_control.imageset/Contents.json b/Example Apps/Shared/Images.xcassets/remote_control.imageset/Contents.json new file mode 100644 index 000000000..bbc03879b --- /dev/null +++ b/Example Apps/Shared/Images.xcassets/remote_control.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "remote_control_icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example Apps/Shared/Images.xcassets/remote_control.imageset/remote_control_icon.png b/Example Apps/Shared/Images.xcassets/remote_control.imageset/remote_control_icon.png Binary files differnew file mode 100644 index 000000000..95b5dcec5 --- /dev/null +++ b/Example Apps/Shared/Images.xcassets/remote_control.imageset/remote_control_icon.png diff --git a/SmartDeviceLink-iOS.xcodeproj/project.pbxproj b/SmartDeviceLink-iOS.xcodeproj/project.pbxproj index 4476e9e0c..ddaeb9a4c 100644 --- a/SmartDeviceLink-iOS.xcodeproj/project.pbxproj +++ b/SmartDeviceLink-iOS.xcodeproj/project.pbxproj @@ -234,6 +234,8 @@ 1680B11C1A9CD7AD00DBD79E /* SDLProtocolMessageAssemblerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 1680B1101A9CD7AD00DBD79E /* SDLProtocolMessageAssemblerSpec.m */; }; 1680B11D1A9CD7AD00DBD79E /* SDLProtocolMessageDisassemblerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 1680B1111A9CD7AD00DBD79E /* SDLProtocolMessageDisassemblerSpec.m */; }; 1680B11E1A9CD7AD00DBD79E /* SDLProtocolReceivedMessageRouterSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 1680B1121A9CD7AD00DBD79E /* SDLProtocolReceivedMessageRouterSpec.m */; }; + 1C87BFB228981328006E79F1 /* RemoteControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C87BFB128981328006E79F1 /* RemoteControlManager.m */; }; + 1CFDAAA72893181200332B84 /* RemoteControlManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CFDAAA62893181200332B84 /* RemoteControlManager.swift */; }; 1E89B0DE2031636000A47992 /* SDLSeatControlDataSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E89B0DD2031636000A47992 /* SDLSeatControlDataSpec.m */; }; 1E89B0E2203196B800A47992 /* SDLSeatControlCapabilitiesSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E89B0E1203196B800A47992 /* SDLSeatControlCapabilitiesSpec.m */; }; 1EAA470E2032BF1D000FE74B /* SDLOnRCStatusSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EAA470D2032BF1D000FE74B /* SDLOnRCStatusSpec.m */; }; @@ -2100,6 +2102,9 @@ 1680B1101A9CD7AD00DBD79E /* SDLProtocolMessageAssemblerSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDLProtocolMessageAssemblerSpec.m; sourceTree = "<group>"; }; 1680B1111A9CD7AD00DBD79E /* SDLProtocolMessageDisassemblerSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDLProtocolMessageDisassemblerSpec.m; sourceTree = "<group>"; }; 1680B1121A9CD7AD00DBD79E /* SDLProtocolReceivedMessageRouterSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDLProtocolReceivedMessageRouterSpec.m; sourceTree = "<group>"; }; + 1C87BFB028981328006E79F1 /* RemoteControlManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RemoteControlManager.h; path = "Example Apps/Example ObjC/RemoteControlManager.h"; sourceTree = SOURCE_ROOT; }; + 1C87BFB128981328006E79F1 /* RemoteControlManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RemoteControlManager.m; path = "Example Apps/Example ObjC/RemoteControlManager.m"; sourceTree = SOURCE_ROOT; }; + 1CFDAAA62893181200332B84 /* RemoteControlManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RemoteControlManager.swift; path = "Example Apps/Example Swift/RemoteControlManager.swift"; sourceTree = SOURCE_ROOT; }; 1E89B0DD2031636000A47992 /* SDLSeatControlDataSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDLSeatControlDataSpec.m; sourceTree = "<group>"; }; 1E89B0E1203196B800A47992 /* SDLSeatControlCapabilitiesSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDLSeatControlCapabilitiesSpec.m; sourceTree = "<group>"; }; 1EAA470D2032BF1D000FE74B /* SDLOnRCStatusSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDLOnRCStatusSpec.m; sourceTree = "<group>"; }; @@ -4789,7 +4794,7 @@ 88295677207CF46C00EF056C /* Objective-C */, ); name = Examples; - path = SmartDeviceLink_Example; + path = "Example Apps"; sourceTree = "<group>"; }; 5D4019B21A76EC350006B0C2 /* Supporting Files */ = { @@ -4902,6 +4907,8 @@ 4A40255B250026620080E159 /* SubscribeButtonManager.m */, 5D1FF29021304513000EB9B4 /* VehicleDataManager.h */, 5D1FF29921304514000EB9B4 /* VehicleDataManager.m */, + 1C87BFB028981328006E79F1 /* RemoteControlManager.h */, + 1C87BFB128981328006E79F1 /* RemoteControlManager.m */, ); name = SDL; sourceTree = "<group>"; @@ -6866,6 +6873,7 @@ 5D1FF2D521304746000EB9B4 /* RPCPermissionsManager.swift */, 4A402558250026430080E159 /* SubscribeButtonManager.swift */, 5D1FF2D921304746000EB9B4 /* VehicleDataManager.swift */, + 1CFDAAA62893181200332B84 /* RemoteControlManager.swift */, ); name = SDL; sourceTree = "<group>"; @@ -8100,6 +8108,7 @@ 5D1FF2A221304515000EB9B4 /* ButtonManager.m in Sources */, 5D1FF2A021304515000EB9B4 /* AudioManager.m in Sources */, 5D1FF29E21304515000EB9B4 /* MenuManager.m in Sources */, + 1C87BFB228981328006E79F1 /* RemoteControlManager.m in Sources */, 4ADBD1FB26FCEDFC00ABB045 /* ConnectionTabBarController.m in Sources */, 5D1FF2C3213045EB000EB9B4 /* AppConstants.m in Sources */, 5D1FF2B821304581000EB9B4 /* ConnectionIAPTableViewController.m in Sources */, @@ -9249,6 +9258,7 @@ 5D1FF2DF21304746000EB9B4 /* PerformInteractionManager.swift in Sources */, 5D1FF2ED2130479C000EB9B4 /* ConnectionIAPTableViewController.swift in Sources */, 5D1FF2DD21304746000EB9B4 /* RPCPermissionsManager.swift in Sources */, + 1CFDAAA72893181200332B84 /* RemoteControlManager.swift in Sources */, 5D1FF2E721304761000EB9B4 /* TextValidator.swift in Sources */, 4A402559250026430080E159 /* SubscribeButtonManager.swift in Sources */, 5D1FF2F8213047C1000EB9B4 /* AppDelegate.swift in Sources */, diff --git a/SmartDeviceLink-iOS.xcodeproj/xcshareddata/xcschemes/SmartDeviceLinkTests.xcscheme b/SmartDeviceLink-iOS.xcodeproj/xcshareddata/xcschemes/SmartDeviceLinkTests.xcscheme new file mode 100644 index 000000000..2a19af5b0 --- /dev/null +++ b/SmartDeviceLink-iOS.xcodeproj/xcshareddata/xcschemes/SmartDeviceLinkTests.xcscheme @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1340" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES"> + <Testables> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "5D61FA251A84237100846EE7" + BuildableName = "SmartDeviceLinkTests.xctest" + BlueprintName = "SmartDeviceLinkTests" + ReferencedContainer = "container:SmartDeviceLink-iOS.xcodeproj"> + </BuildableReference> + </TestableReference> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> |