summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Fischer <joeljfischer@gmail.com>2022-08-10 15:54:35 -0400
committerGitHub <noreply@github.com>2022-08-10 15:54:35 -0400
commitaba5ba83d902a1c5e7e68915180eaae319855430 (patch)
tree54ae427cd27fac366318c6e42606759e2de8e9ba
parent42393ce4226a3a1742a164a7be1c8045a07b215f (diff)
parentb270c572be22de19376c74b2d8a98206926ef2e7 (diff)
downloadsdl_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.h3
-rw-r--r--Example Apps/Example ObjC/MenuManager.m55
-rw-r--r--Example Apps/Example ObjC/ProxyManager.m28
-rw-r--r--Example Apps/Example ObjC/RemoteControlManager.h29
-rw-r--r--Example Apps/Example ObjC/RemoteControlManager.m247
-rw-r--r--Example Apps/Example Swift/MenuManager.swift58
-rw-r--r--Example Apps/Example Swift/ProxyManager.swift25
-rw-r--r--Example Apps/Example Swift/RemoteControlManager.swift226
-rw-r--r--Example Apps/Shared/AppConstants.h5
-rw-r--r--Example Apps/Shared/AppConstants.m5
-rw-r--r--Example Apps/Shared/Images.xcassets/remote_control.imageset/Contents.json21
-rw-r--r--Example Apps/Shared/Images.xcassets/remote_control.imageset/remote_control_icon.pngbin0 -> 2146 bytes
-rw-r--r--SmartDeviceLink-iOS.xcodeproj/project.pbxproj12
-rw-r--r--SmartDeviceLink-iOS.xcodeproj/xcshareddata/xcschemes/SmartDeviceLinkTests.xcscheme52
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
new file mode 100644
index 000000000..95b5dcec5
--- /dev/null
+++ b/Example Apps/Shared/Images.xcassets/remote_control.imageset/remote_control_icon.png
Binary files differ
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>