diff options
author | NicoleYarroch <nicole@livio.io> | 2019-02-21 15:57:09 -0500 |
---|---|---|
committer | NicoleYarroch <nicole@livio.io> | 2019-02-21 15:57:09 -0500 |
commit | f1f2d968630266cf83cbbc4751cddb7e5f358394 (patch) | |
tree | cc003494328c332c24d587cac9a58e3851ca8381 | |
parent | 0d30a06df3901482dbffa4be665211ee98133383 (diff) | |
parent | 6920985a50b8eebb4d919a2589d47250964daa37 (diff) | |
download | sdl_ios-f1f2d968630266cf83cbbc4751cddb7e5f358394.tar.gz |
Merge branch 'develop' into feature/issue_1147_and_1148_app_services_weather_media
# Conflicts:
# SmartDeviceLink-iOS.xcodeproj/project.pbxproj
-rw-r--r-- | SmartDeviceLink-iOS.xcodeproj/project.pbxproj | 30 | ||||
-rw-r--r-- | SmartDeviceLink/SDLIAPSession.m | 46 | ||||
-rw-r--r-- | SmartDeviceLink/SDLIAPTransport.h | 4 | ||||
-rw-r--r-- | SmartDeviceLink/SDLIAPTransport.m | 84 | ||||
-rw-r--r-- | SmartDeviceLink/SDLLifecycleManager.m | 4 | ||||
-rw-r--r-- | SmartDeviceLinkTests/TransportSpecs/TCP/SDLTCPTransportSpec.m (renamed from SmartDeviceLinkTests/TransportSpecs/SDLTCPTransportSpec.m) | 0 | ||||
-rw-r--r-- | SmartDeviceLinkTests/TransportSpecs/iAP/EAAccessory+OCMock.m | 62 | ||||
-rw-r--r-- | SmartDeviceLinkTests/TransportSpecs/iAP/SDLIAPSessionSpec.m | 165 | ||||
-rw-r--r-- | SmartDeviceLinkTests/TransportSpecs/iAP/SDLIAPTransportSpec.m | 118 |
9 files changed, 470 insertions, 43 deletions
diff --git a/SmartDeviceLink-iOS.xcodeproj/project.pbxproj b/SmartDeviceLink-iOS.xcodeproj/project.pbxproj index 147b55bec..517b70ad7 100644 --- a/SmartDeviceLink-iOS.xcodeproj/project.pbxproj +++ b/SmartDeviceLink-iOS.xcodeproj/project.pbxproj @@ -1325,6 +1325,9 @@ 88EEC5BB220A327B005AA2F9 /* SDLPublishAppServiceResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 88EEC5B9220A327B005AA2F9 /* SDLPublishAppServiceResponse.h */; settings = {ATTRIBUTES = (Public, ); }; }; 88EEC5BC220A327B005AA2F9 /* SDLPublishAppServiceResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 88EEC5BA220A327B005AA2F9 /* SDLPublishAppServiceResponse.m */; }; 88EEC5BE220A3B8B005AA2F9 /* SDLPublishAppServiceResponseSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 88EEC5BD220A3B8B005AA2F9 /* SDLPublishAppServiceResponseSpec.m */; }; + 88DF998D22035CC600477AC1 /* EAAccessory+OCMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 88DF998C22035CC600477AC1 /* EAAccessory+OCMock.m */; }; + 88DF998F22035D1700477AC1 /* SDLIAPSessionSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 88DF998E22035D1700477AC1 /* SDLIAPSessionSpec.m */; }; + 88DF999122035D5A00477AC1 /* SDLIAPTransportSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 88DF999022035D5A00477AC1 /* SDLIAPTransportSpec.m */; }; 88EED8381F33AE1700E6C42E /* SDLHapticRect.h in Headers */ = {isa = PBXBuildFile; fileRef = 88EED8361F33AE1700E6C42E /* SDLHapticRect.h */; settings = {ATTRIBUTES = (Public, ); }; }; 88EED8391F33AE1700E6C42E /* SDLHapticRect.m in Sources */ = {isa = PBXBuildFile; fileRef = 88EED8371F33AE1700E6C42E /* SDLHapticRect.m */; }; 88EED83B1F33BECB00E6C42E /* SDLHapticRectSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 88EED83A1F33BECB00E6C42E /* SDLHapticRectSpec.m */; }; @@ -2899,6 +2902,9 @@ 88EEC5B9220A327B005AA2F9 /* SDLPublishAppServiceResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDLPublishAppServiceResponse.h; sourceTree = "<group>"; }; 88EEC5BA220A327B005AA2F9 /* SDLPublishAppServiceResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDLPublishAppServiceResponse.m; sourceTree = "<group>"; }; 88EEC5BD220A3B8B005AA2F9 /* SDLPublishAppServiceResponseSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDLPublishAppServiceResponseSpec.m; sourceTree = "<group>"; }; + 88DF998C22035CC600477AC1 /* EAAccessory+OCMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "EAAccessory+OCMock.m"; sourceTree = "<group>"; }; + 88DF998E22035D1700477AC1 /* SDLIAPSessionSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDLIAPSessionSpec.m; sourceTree = "<group>"; }; + 88DF999022035D5A00477AC1 /* SDLIAPTransportSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDLIAPTransportSpec.m; sourceTree = "<group>"; }; 88EED8361F33AE1700E6C42E /* SDLHapticRect.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDLHapticRect.h; sourceTree = "<group>"; }; 88EED8371F33AE1700E6C42E /* SDLHapticRect.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDLHapticRect.m; sourceTree = "<group>"; }; 88EED83A1F33BECB00E6C42E /* SDLHapticRectSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDLHapticRectSpec.m; sourceTree = "<group>"; }; @@ -5725,6 +5731,24 @@ name = Helpers; sourceTree = "<group>"; }; + 88DF998A22035CA400477AC1 /* TCP */ = { + isa = PBXGroup; + children = ( + EE5D1B32208EBCA900D17216 /* SDLTCPTransportSpec.m */, + ); + path = TCP; + sourceTree = "<group>"; + }; + 88DF998B22035CB100477AC1 /* iAP */ = { + isa = PBXGroup; + children = ( + 88DF998C22035CC600477AC1 /* EAAccessory+OCMock.m */, + 88DF998E22035D1700477AC1 /* SDLIAPSessionSpec.m */, + 88DF999022035D5A00477AC1 /* SDLIAPTransportSpec.m */, + ); + path = iAP; + sourceTree = "<group>"; + }; DA1166D71D14601C00438CEA /* Touches */ = { isa = PBXGroup; children = ( @@ -5837,7 +5861,8 @@ EE5D1B31208EBC7100D17216 /* TransportSpecs */ = { isa = PBXGroup; children = ( - EE5D1B32208EBCA900D17216 /* SDLTCPTransportSpec.m */, + 88DF998B22035CB100477AC1 /* iAP */, + 88DF998A22035CA400477AC1 /* TCP */, ); path = TransportSpecs; sourceTree = "<group>"; @@ -7086,6 +7111,7 @@ 1EB59CCE202DC97900343A61 /* SDLMassageCushionSpec.m in Sources */, 162E83041A9BDE8B00906325 /* SDLUpdateModeSpec.m in Sources */, 8855F9E0220C93B700A5C897 /* SDLWeatherDataSpec.m in Sources */, + 88DF998D22035CC600477AC1 /* EAAccessory+OCMock.m in Sources */, 162E83801A9BDE8B00906325 /* SDLHMIPermissionsSpec.m in Sources */, 1EAA476C2036A52F000FE74B /* SDLLightCapabilitiesSpec.m in Sources */, 5D1654561D3E754F00554D93 /* SDLLifecycleManagerSpec.m in Sources */, @@ -7093,6 +7119,7 @@ 162E83021A9BDE8B00906325 /* SDLTouchTypeSpec.m in Sources */, 1EAA47722036AEF5000FE74B /* SDLLightNameSpec.m in Sources */, 5DB92D2F1AC59F0000C15BB0 /* SDLObjectWithPrioritySpec.m in Sources */, + 88DF999122035D5A00477AC1 /* SDLIAPTransportSpec.m in Sources */, 162E838A1A9BDE8B00906325 /* SDLSingleTireStatusSpec.m in Sources */, 5D6EB4CC1BF28DC600693731 /* NSMapTable+SubscriptingSpec.m in Sources */, 162E83051A9BDE8B00906325 /* SDLVehicleDataActiveStatusSpec.m in Sources */, @@ -7285,6 +7312,7 @@ 162E83141A9BDE8B00906325 /* SDLOnDriverDistractionSpec.m in Sources */, 162E83371A9BDE8B00906325 /* SDLResetGlobalPropertiesSpec.m in Sources */, 162E82DF1A9BDE8B00906325 /* SDLGlobalProperySpec.m in Sources */, + 88DF998F22035D1700477AC1 /* SDLIAPSessionSpec.m in Sources */, 5DD8406520FCE21A0082CE04 /* SDLElectronicParkBrakeStatusSpec.m in Sources */, 162E82F61A9BDE8B00906325 /* SDLRequestTypeSpec.m in Sources */, 5DE35E4520CAFC5D0034BE5A /* SDLChoiceCellSpec.m in Sources */, diff --git a/SmartDeviceLink/SDLIAPSession.m b/SmartDeviceLink/SDLIAPSession.m index 98cc35813..6fc39be9a 100644 --- a/SmartDeviceLink/SDLIAPSession.m +++ b/SmartDeviceLink/SDLIAPSession.m @@ -49,15 +49,19 @@ NSTimeInterval const StreamThreadWaitSecs = 10.0; #pragma mark - Public Stream Lifecycle - (BOOL)start { - __weak typeof(self) weakSelf = self; SDLLogD(@"Opening EASession withAccessory:%@ forProtocol:%@", _accessory.name, _protocol); + self.easession = [[EASession alloc] initWithAccessory:self.accessory forProtocol:self.protocol]; + return [self sdl_startWithSession:self.easession]; +} - // TODO: This assignment should be broken out of the if and the if / else should be flipped. - if ((self.easession = [[EASession alloc] initWithAccessory:self.accessory forProtocol:self.protocol])) { +- (BOOL)sdl_startWithSession:(EASession *)session { + __weak typeof(self) weakSelf = self; + if (session == nil) { + SDLLogE(@"Error creating the session object"); + return NO; + } else { + SDLLogD(@"Created the session object successfully"); __strong typeof(self) strongSelf = weakSelf; - - SDLLogD(@"Created Session Object"); - strongSelf.streamDelegate.streamErrorHandler = [self streamErroredHandler]; strongSelf.streamDelegate.streamOpenHandler = [self streamOpenedHandler]; if (self.isDataSession) { @@ -72,10 +76,6 @@ NSTimeInterval const StreamThreadWaitSecs = 10.0; [self startStream:self.easession.inputStream]; } return YES; - - } else { - SDLLogE(@"Error: Could Not Create Session Object"); - return NO; } } @@ -94,14 +94,13 @@ NSTimeInterval const StreamThreadWaitSecs = 10.0; if (self.isDataSession) { [self.ioStreamThread cancel]; - long lWait = dispatch_semaphore_wait(self.canceledSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(StreamThreadWaitSecs * NSEC_PER_SEC))); - if (lWait == 0) { - SDLLogW(@"Stream thread cancelled"); - } else { - SDLLogE(@"Failed to cancel stream thread"); - } - self.ioStreamThread = nil; - self.isDataSession = NO; + [self sdl_isIOThreadCanceled:self.canceledSemaphore completionHandler:^(BOOL success) { + if (success == NO) { + SDLLogE(@"About to destroy a thread that has not yet closed."); + } + self.ioStreamThread = nil; + self.isDataSession = NO; + }]; } else { // Stop control session [self stopStream:self.easession.outputStream]; @@ -110,6 +109,17 @@ NSTimeInterval const StreamThreadWaitSecs = 10.0; self.easession = nil; } +- (void)sdl_isIOThreadCanceled:(dispatch_semaphore_t)canceledSemaphore completionHandler:(void (^)(BOOL success))completionHandler { + long lWait = dispatch_semaphore_wait(canceledSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(StreamThreadWaitSecs * NSEC_PER_SEC))); + if (lWait == 0) { + SDLLogD(@"Stream thread canceled successfully"); + return completionHandler(YES); + } else { + SDLLogE(@"Failed to cancel stream thread"); + return completionHandler(NO); + } +} + - (BOOL)isStopped { return !self.isOutputStreamOpen && !self.isInputStreamOpen; } diff --git a/SmartDeviceLink/SDLIAPTransport.h b/SmartDeviceLink/SDLIAPTransport.h index ec32ae1f1..6934ffc27 100644 --- a/SmartDeviceLink/SDLIAPTransport.h +++ b/SmartDeviceLink/SDLIAPTransport.h @@ -11,12 +11,12 @@ NS_ASSUME_NONNULL_BEGIN @interface SDLIAPTransport : NSObject <SDLTransportType, SDLIAPSessionDelegate> /** - * Session for transporting data between the app and Core. + * Session for establishing a connection with Core. Once the connection has been established, the session is closed and a session is established. A `controlSession` is not needed if the head unit supports the multisession protocol string. */ @property (nullable, strong, nonatomic) SDLIAPSession *controlSession; /** - * Session for establishing a connection with Core. Once the connection has been established, the session is closed and a control session is established. + * Session for transporting data between the app and Core. */ @property (nullable, strong, nonatomic) SDLIAPSession *session; diff --git a/SmartDeviceLink/SDLIAPTransport.m b/SmartDeviceLink/SDLIAPTransport.m index f4a3698f5..b0fa07177 100644 --- a/SmartDeviceLink/SDLIAPTransport.m +++ b/SmartDeviceLink/SDLIAPTransport.m @@ -35,7 +35,7 @@ int const ProtocolIndexTimeoutSeconds = 10; @property (assign, nonatomic) BOOL sessionSetupInProgress; @property (nonatomic, assign) UIBackgroundTaskIdentifier backgroundTaskId; @property (nullable, strong, nonatomic) SDLTimer *protocolIndexTimer; - +@property (assign, nonatomic) BOOL accessoryConnectDuringActiveSession; @end @@ -50,6 +50,7 @@ int const ProtocolIndexTimeoutSeconds = 10; _controlSession = nil; _retryCounter = 0; _protocolIndexTimer = nil; + _accessoryConnectDuringActiveSession = NO; // Get notifications if an accessory connects in future [self sdl_startEventListening]; @@ -67,14 +68,16 @@ int const ProtocolIndexTimeoutSeconds = 10; */ - (void)sdl_backgroundTaskStart { if (self.backgroundTaskId != UIBackgroundTaskInvalid) { + SDLLogV(@"A background task is already running. No need to start a background task. Returning..."); return; } - SDLLogD(@"Starting background task"); self.backgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithName:BackgroundTaskName expirationHandler:^{ SDLLogD(@"Background task expired"); [self sdl_backgroundTaskEnd]; }]; + + SDLLogD(@"Started a background task with id: %lu", (unsigned long)self.backgroundTaskId); } /** @@ -82,10 +85,11 @@ int const ProtocolIndexTimeoutSeconds = 10; */ - (void)sdl_backgroundTaskEnd { if (self.backgroundTaskId == UIBackgroundTaskInvalid) { + SDLLogV(@"No background task running. No need to stop the background task. Returning..."); return; } - SDLLogD(@"Ending background task"); + SDLLogD(@"Ending background task with id: %lu", (unsigned long)self.backgroundTaskId); [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskId]; self.backgroundTaskId = UIBackgroundTaskInvalid; } @@ -139,7 +143,14 @@ int const ProtocolIndexTimeoutSeconds = 10; * @param notification Contains information about the connected accessory */ - (void)sdl_accessoryConnected:(NSNotification *)notification { - double retryDelay = self.retryDelay; + EAAccessory *newAccessory = [notification.userInfo objectForKey:EAAccessoryKey]; + + if ([self sdl_isSessionActive:self.session newAccessory:newAccessory]) { + self.accessoryConnectDuringActiveSession = YES; + return; + } + + double retryDelay = self.sdl_retryDelay; SDLLogD(@"Accessory Connected (%@), Opening in %0.03fs", notification.userInfo[EAAccessoryKey], retryDelay); if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) { @@ -152,23 +163,53 @@ int const ProtocolIndexTimeoutSeconds = 10; } /** + * Checks if the newly connected accessory connected while a data session is already opened. This can happen when a session is established over bluetooth and then the user connects to the same head unit with a USB cord. + * + * @param session The current data session, which may be nil + * @param newAccessory The newly connected accessory + * @return True if the accessory connected while a data session is already in progress; false if not + */ +- (BOOL)sdl_isSessionActive:(SDLIAPSession *)session newAccessory:(EAAccessory *)newAccessory { + if ((session != nil) && (session.accessory.connectionID != newAccessory.connectionID)) { + SDLLogD(@"Switching transports from Bluetooth to USB. Waiting for disconnect notification."); + return YES; + } + + return NO; +} + +/** * Handles a notification sent by the system when an accessory has been disconnected by cleaning up after the disconnected device. Only check for the data session, the control session is handled separately * * @param notification Contains information about the connected accessory */ - (void)sdl_accessoryDisconnected:(NSNotification *)notification { EAAccessory *accessory = [notification.userInfo objectForKey:EAAccessoryKey]; - if (accessory.connectionID != self.session.accessory.connectionID) { - SDLLogV(@"Accessory disconnected during control session (%@)", accessory); - self.retryCounter = 0; + SDLLogD(@"Accessory with serial number %@ and connectionID %lu disconnecting.", accessory.serialNumber, (unsigned long)accessory.connectionID); + + if (self.accessoryConnectDuringActiveSession == YES) { + SDLLogD(@"Switching transports from Bluetooth to USB. Will reconnect over Bluetooth after disconnecting the USB session."); + self.accessoryConnectDuringActiveSession = NO; } - if ([accessory.serialNumber isEqualToString:self.session.accessory.serialNumber]) { - SDLLogV(@"Accessory disconnected during data session (%@)", accessory); - self.retryCounter = 0; - self.sessionSetupInProgress = NO; - [self disconnect]; - [self.delegate onTransportDisconnected]; + + if (self.controlSession == nil && self.session == nil) { + SDLLogV(@"Accessory (%@), disconnected, but no session is in progress", accessory.serialNumber); + } else if (accessory.connectionID == self.controlSession.accessory.connectionID) { + SDLLogV(@"Accessory (%@) disconnected during a control session", accessory.serialNumber); + } else if (accessory.connectionID == self.session.accessory.connectionID) { + SDLLogV(@"Accessory (%@) disconnected during a data session", accessory.serialNumber); + } else { + SDLLogV(@"Accessory (%@) disconnecting during an unknown session", accessory.serialNumber); } + + [self sdl_destroySession]; +} + +- (void)sdl_destroySession { + self.retryCounter = 0; + self.sessionSetupInProgress = NO; + [self disconnect]; + [self.delegate onTransportDisconnected]; } #pragma mark App Lifecycle Notifications @@ -232,14 +273,15 @@ int const ProtocolIndexTimeoutSeconds = 10; * Cleans up after a disconnected accessory by closing any open input streams. */ - (void)disconnect { - SDLLogD(@"Disconnecting IAP data session"); // Stop event listening here so that even if the transport is disconnected by the proxy we unregister for accessory local notifications [self sdl_stopEventListening]; if (self.controlSession != nil) { + SDLLogD(@"Disconnecting control session"); [self.controlSession stop]; self.controlSession.streamDelegate = nil; self.controlSession = nil; } else if (self.session != nil) { + SDLLogD(@"Disconnecting data session"); [self.session stop]; self.session.streamDelegate = nil; self.session = nil; @@ -304,20 +346,20 @@ int const ProtocolIndexTimeoutSeconds = 10; } /** - * Attept to establish a session with an accessory, or if nil is passed, to scan for one. + * Attempt to establish a session with an accessory, or if nil is passed, to scan for one. * * @param accessory The accessory to try to establish a session with, or nil to scan all connected accessories. */ - (void)sdl_establishSessionWithAccessory:(nullable EAAccessory *)accessory { - SDLLogD(@"Attempting to connect"); + SDLLogD(@"Attempting to connect accessory: %@", accessory.name); if (self.retryCounter < CreateSessionRetries) { - // We should be attempting to connect self.retryCounter++; EAAccessory *sdlAccessory = accessory; // If we are being called from sdl_connectAccessory, the EAAccessoryDidConnectNotification will contain the SDL accessory to connect to and we can connect without searching the accessory manager's connected accessory list. Otherwise, we fall through to a search. if (sdlAccessory != nil && [self sdl_connectAccessory:sdlAccessory]) { // Connection underway, exit + SDLLogV(@"Connection already underway"); return; } @@ -340,10 +382,9 @@ int const ProtocolIndexTimeoutSeconds = 10; SDLLogV(@"No accessory supporting SDL was found, dismissing setup"); self.sessionSetupInProgress = NO; } - } else { - // We are beyond the number of retries allowed - SDLLogW(@"Surpassed allowed retry attempts"); + // We have surpassed the number of retries allowed + SDLLogW(@"Surpassed allowed retry attempts (%d), dismissing setup", CreateSessionRetries); self.sessionSetupInProgress = NO; } } @@ -619,7 +660,7 @@ int const ProtocolIndexTimeoutSeconds = 10; }; } -- (double)retryDelay { +- (double)sdl_retryDelay { const double MinRetrySeconds = 1.5; const double MaxRetrySeconds = 9.5; double RetryRangeSeconds = MaxRetrySeconds - MinRetrySeconds; @@ -668,6 +709,7 @@ int const ProtocolIndexTimeoutSeconds = 10; self.session = nil; self.delegate = nil; self.sessionSetupInProgress = NO; + self.accessoryConnectDuringActiveSession = NO; } } diff --git a/SmartDeviceLink/SDLLifecycleManager.m b/SmartDeviceLink/SDLLifecycleManager.m index d521244d0..5436d8945 100644 --- a/SmartDeviceLink/SDLLifecycleManager.m +++ b/SmartDeviceLink/SDLLifecycleManager.m @@ -290,7 +290,9 @@ SDLLifecycleState *const SDLLifecycleStateReady = @"Ready"; // If the success BOOL is NO or we received an error at this point, we failed. Call the ready handler and transition to the DISCONNECTED state. if (error != nil || ![response.success boolValue]) { SDLLogE(@"Failed to register the app. Error: %@, Response: %@", error, response); - weakSelf.readyHandler(NO, error); + if (weakSelf.readyHandler) { + weakSelf.readyHandler(NO, error); + } if (weakSelf.lifecycleState != SDLLifecycleStateReconnecting) { [weakSelf sdl_transitionToState:SDLLifecycleStateStopped]; diff --git a/SmartDeviceLinkTests/TransportSpecs/SDLTCPTransportSpec.m b/SmartDeviceLinkTests/TransportSpecs/TCP/SDLTCPTransportSpec.m index e9a25f834..e9a25f834 100644 --- a/SmartDeviceLinkTests/TransportSpecs/SDLTCPTransportSpec.m +++ b/SmartDeviceLinkTests/TransportSpecs/TCP/SDLTCPTransportSpec.m diff --git a/SmartDeviceLinkTests/TransportSpecs/iAP/EAAccessory+OCMock.m b/SmartDeviceLinkTests/TransportSpecs/iAP/EAAccessory+OCMock.m new file mode 100644 index 000000000..1c32c4b73 --- /dev/null +++ b/SmartDeviceLinkTests/TransportSpecs/iAP/EAAccessory+OCMock.m @@ -0,0 +1,62 @@ +// +// EAAccessory+OCMock.m +// SmartDeviceLinkTests +// +// Created by Nicole on 1/24/19. +// Copyright © 2019 smartdevicelink. All rights reserved. +// + +#import <Foundation/Foundation.h> +#import <UIKit/UIKit.h> +#import <OCMock/OCMock.h> +#import <ExternalAccessory/ExternalAccessory.h> + +// Based off of the Pebble Accessory OCKMock by Heiko Behrens https://github.com/HBehrens/PebbleKit-ios-sdk-test/blob/master/PebbleVendor/EAAccessoryFramework%2BOCMock.m + +@implementation EAAccessory (OCMock) +static id coreMockDelegate = nil; ++ (EAAccessory *)sdlCoreMock { + id mockEAAccessory = OCMClassMock([EAAccessory class]); + OCMStub([mockEAAccessory protocolStrings]).andReturn(@[@"com.smartdevicelink.multisession"]); + [[[mockEAAccessory stub] andReturnValue:OCMOCK_VALUE((NSString *)@"SDLTestHeadUnit")] name]; + OCMStub([mockEAAccessory modelNumber]).andReturn(@"0.0.0"); + OCMStub([mockEAAccessory serialNumber]).andReturn(@"123456"); + OCMStub([mockEAAccessory firmwareRevision]).andReturn(@"1.2.3"); + OCMStub([mockEAAccessory hardwareRevision]).andReturn(@"3.2.1"); + OCMStub([mockEAAccessory isConnected]).andReturn(OCMOCK_VALUE(YES)); + OCMStub([mockEAAccessory setDelegate:[OCMArg checkWithBlock:^BOOL(id obj) { + coreMockDelegate = obj; + return YES; + }]]); + OCMStub([mockEAAccessory delegate]).andCall(self, @selector(coreDelegate)); + [[[mockEAAccessory stub] andReturnValue:OCMOCK_VALUE((NSUInteger){5})] connectionID]; + + return mockEAAccessory; +} +- (id)coreDelegate { + return coreMockDelegate; +} +@end + +@implementation EAAccessoryManager (OCMock) ++ (EAAccessoryManager *)mockManager { + id mockEAAccessoryManager = OCMClassMock([EAAccessoryManager class]); + id mockEAAccessory = [EAAccessory sdlCoreMock]; + OCMStub([mockEAAccessoryManager connectedAccessories]).andReturn(@[mockEAAccessory]); + OCMStub([mockEAAccessory registerForLocalNotifications]); + OCMStub([mockEAAccessory unregisterForLocalNotifications]); + return mockEAAccessoryManager; +} +@end + +@implementation EASession (OCMock) ++ (EASession *)mockSessionWithAccessory:(EAAccessory*)mockAccessory protocolString:(NSString*)mockProtocolString inputStream:(NSInputStream*)mockInputStream outputStream:(NSOutputStream*)mockOutputStream { + id session = OCMClassMock([EASession class]); + OCMStub([session accessory]).andReturn(mockAccessory); + OCMStub([session protocolString]).andReturn(mockProtocolString); + OCMStub([session inputStream]).andReturn(mockInputStream); + OCMStub([session outputStream]).andReturn(mockOutputStream); + return session; +} +@end + diff --git a/SmartDeviceLinkTests/TransportSpecs/iAP/SDLIAPSessionSpec.m b/SmartDeviceLinkTests/TransportSpecs/iAP/SDLIAPSessionSpec.m new file mode 100644 index 000000000..672aa76b1 --- /dev/null +++ b/SmartDeviceLinkTests/TransportSpecs/iAP/SDLIAPSessionSpec.m @@ -0,0 +1,165 @@ +// +// SDLIAPSessionSpec.m +// SmartDeviceLinkTests +// +// Created by Nicole on 1/23/19. +// Copyright © 2019 smartdevicelink. All rights reserved. +// + +#import <Quick/Quick.h> +#import <Nimble/Nimble.h> +#import <OCMock/OCMock.h> + +#import "EAAccessory+OCMock.m" +#import "SDLIAPSession.h" +#import "SDLMutableDataQueue.h" +#import "SDLStreamDelegate.h" + +@interface SDLIAPSession () +@property (nonatomic, assign) BOOL isInputStreamOpen; +@property (nonatomic, assign) BOOL isOutputStreamOpen; +@property (nonatomic, assign) BOOL isDataSession; +@property (nullable, nonatomic, strong) NSThread *ioStreamThread; +@property (nonatomic, strong) SDLMutableDataQueue *sendDataQueue; +@property (nonatomic, strong) dispatch_semaphore_t canceledSemaphore; +- (BOOL)sdl_startWithSession:(EASession *)session; +@end + +QuickSpecBegin(SDLIAPSessionSpec) + +describe(@"SDLIAPSession", ^{ + __block SDLIAPSession *iapSession = nil; + __block EAAccessory *mockAccessory = nil; + __block NSString *protocol = nil; + + describe(@"Initialization", ^{ + beforeEach(^{ + mockAccessory = [EAAccessory.class sdlCoreMock]; + }); + + it(@"Should init correctly with a control protocol string", ^{ + protocol = @"com.smartdevicelink.prot0"; + iapSession = [[SDLIAPSession alloc] initWithAccessory:mockAccessory forProtocol:protocol]; + + expect(iapSession.isDataSession).to(beFalse()); + }); + + it(@"Should init correctly with a multisession protocol string", ^{ + protocol = @"com.smartdevicelink.multisession"; + iapSession = [[SDLIAPSession alloc] initWithAccessory:mockAccessory forProtocol:protocol]; + + expect(iapSession.isDataSession).to(beTrue()); + }); + + it(@"Should init correctly with a legacy protocol string", ^{ + protocol = @"com.ford.sync.prot0"; + iapSession = [[SDLIAPSession alloc] initWithAccessory:mockAccessory forProtocol:protocol]; + + expect(iapSession.isDataSession).to(beTrue()); + }); + + it(@"Should init correctly with a indexed protocol string", ^{ + protocol = @"com.smartdevicelink.prot1"; + iapSession = [[SDLIAPSession alloc] initWithAccessory:mockAccessory forProtocol:protocol]; + + expect(iapSession.isDataSession).to(beTrue()); + }); + + afterEach(^{ + expect(iapSession).toNot(beNil()); + expect(iapSession.protocol).to(match(protocol)); + expect(iapSession.accessory).to(equal(mockAccessory)); + expect(iapSession.canceledSemaphore).toNot(beNil()); + expect(iapSession.sendDataQueue).toNot(beNil()); + expect(iapSession.isInputStreamOpen).to(beFalse()); + expect(iapSession.isOutputStreamOpen).to(beFalse()); + }); + }); + + describe(@"When starting a session", ^{ + __block SDLStreamDelegate *streamDelegate = nil; + __block NSInputStream *inputStream = nil; + __block NSOutputStream *outputStream = nil; + + describe(@"unsuccessfully", ^{ + beforeEach(^{ + protocol = @"com.smartdevicelink.multisession"; + mockAccessory = [EAAccessory.class sdlCoreMock]; + iapSession = [[SDLIAPSession alloc] initWithAccessory:mockAccessory forProtocol:protocol]; + streamDelegate = [[SDLStreamDelegate alloc] init]; + iapSession.streamDelegate = streamDelegate; + }); + + it(@"the start method should return false if a session cannot be created", ^{ + BOOL success = [iapSession sdl_startWithSession:nil]; + expect(success).to(beFalse()); + expect(iapSession.ioStreamThread).to(beNil()); + expect(iapSession.isInputStreamOpen).to(beFalse()); + expect(iapSession.isOutputStreamOpen).to(beFalse()); + }); + }); + + describe(@"successfully", ^{ + beforeEach(^{ + inputStream = OCMClassMock([NSInputStream class]); + outputStream = OCMClassMock([NSOutputStream class]); + }); + + context(@"if creating a control session", ^{ + beforeEach(^{ + protocol = @"com.smartdevicelink.prot0"; + mockAccessory = [EAAccessory.class sdlCoreMock]; + iapSession = [[SDLIAPSession alloc] initWithAccessory:mockAccessory forProtocol:protocol]; + streamDelegate = [[SDLStreamDelegate alloc] init]; + iapSession.streamDelegate = streamDelegate; + + expect(iapSession.isDataSession).to(beFalse()); + }); + + it(@"should establish a control session ", ^{ + EASession *mockSession = [EASession.class mockSessionWithAccessory:mockAccessory protocolString:protocol inputStream:inputStream outputStream:outputStream]; + iapSession.easession = mockSession; + + BOOL success = [iapSession sdl_startWithSession:mockSession]; + expect(success).to(beTrue()); + expect(iapSession.ioStreamThread).to(beNil()); + expect(iapSession.easession.inputStream).toNot(beNil()); + expect(iapSession.easession.outputStream).toNot(beNil()); + }); + }); + + context(@"if creating a data session", ^{ + beforeEach(^{ + protocol = @"com.smartdevicelink.multisession"; + mockAccessory = [EAAccessory.class sdlCoreMock]; + iapSession = [[SDLIAPSession alloc] initWithAccessory:mockAccessory forProtocol:protocol]; + streamDelegate = [[SDLStreamDelegate alloc] init]; + iapSession.streamDelegate = streamDelegate; + + expect(iapSession.isDataSession).to(beTrue()); + }); + + it(@"should establish a data session ", ^{ + EASession *mockSession = [EASession.class mockSessionWithAccessory:mockAccessory protocolString:protocol inputStream:inputStream outputStream:outputStream]; + iapSession.easession = mockSession; + BOOL success = [iapSession sdl_startWithSession:mockSession]; + expect(success).to(beTrue()); + expect(iapSession.ioStreamThread).toNot(beNil()); + // The streams are opened in the `sdl_streamHasSpaceHandler` method + expect(iapSession.easession.inputStream).toNot(beNil()); + expect(iapSession.easession.outputStream).toNot(beNil()); + }); + }); + }); + + afterEach(^{ + [iapSession stop]; + + expect(iapSession.easession).to(beNil()); + expect(iapSession.ioStreamThread).to(beNil()); + }); + }); +}); + +QuickSpecEnd + diff --git a/SmartDeviceLinkTests/TransportSpecs/iAP/SDLIAPTransportSpec.m b/SmartDeviceLinkTests/TransportSpecs/iAP/SDLIAPTransportSpec.m new file mode 100644 index 000000000..7223c8d0b --- /dev/null +++ b/SmartDeviceLinkTests/TransportSpecs/iAP/SDLIAPTransportSpec.m @@ -0,0 +1,118 @@ +// +// SDLIAPTransportSpec.m +// SmartDeviceLinkTests +// +// Created by Nicole on 1/23/19. +// Copyright © 2019 smartdevicelink. All rights reserved. +// + +#import <Foundation/Foundation.h> +#import <Quick/Quick.h> +#import <Nimble/Nimble.h> +#import <OCMock/OCMock.h> + +#import "EAAccessory+OCMock.m" +#import "SDLIAPTransport.h" +#import "SDLIAPSession.h" +#import "SDLTimer.h" + +@interface SDLIAPTransport () +@property (assign, nonatomic) int retryCounter; +@property (assign, nonatomic) BOOL sessionSetupInProgress; +@property (nonatomic, assign) UIBackgroundTaskIdentifier backgroundTaskId; +@property (nullable, strong, nonatomic) SDLTimer *protocolIndexTimer; +@property (assign, nonatomic) BOOL accessoryConnectDuringActiveSession; +- (BOOL)sdl_isSessionActive:(SDLIAPSession *)session newAccessory:(EAAccessory *)newAccessory; +@end + +QuickSpecBegin(SDLIAPTransportSpec) + +describe(@"SDLIAPTransport", ^{ + __block SDLIAPTransport *transport = nil; + __block id mockTransportDelegate = nil; + __block EAAccessory *mockAccessory = nil; + + beforeEach(^{ + transport = [SDLIAPTransport new]; + mockTransportDelegate = OCMProtocolMock(@protocol(SDLTransportDelegate)); + transport.delegate = mockTransportDelegate; + mockAccessory = [EAAccessory.class sdlCoreMock]; + }); + + describe(@"Initialization", ^{ + it(@"Should init correctly", ^{ + expect(transport.delegate).toNot(beNil()); + expect(transport.controlSession).to(beNil()); + expect(transport.session).to(beNil()); + expect(transport.sessionSetupInProgress).to(beFalse()); + expect(transport.session).to(beNil()); + expect(transport.controlSession).to(beNil()); + expect(transport.retryCounter).to(equal(0)); + expect(transport.protocolIndexTimer).to(beNil()); + expect(transport.accessoryConnectDuringActiveSession).to(beFalse()); + }); + }); + + describe(@"When an accessory connects while a session is not open", ^{ + beforeEach(^{ + transport.session = nil; + }); + + it(@"If no session is open, it should create a session when an accessory connects", ^{ + BOOL sessionInProgress = [transport sdl_isSessionActive:transport.session newAccessory:mockAccessory]; + expect(sessionInProgress).to(beFalse()); + }); + }); + + describe(@"When an accessory connects when a session is already open", ^{ + beforeEach(^{ + transport.session = OCMClassMock([SDLIAPSession class]); + }); + + it(@"If a session is in progress", ^{ + BOOL sessionInProgress = [transport sdl_isSessionActive:transport.session newAccessory:mockAccessory]; + expect(sessionInProgress).to(beTrue()); + }); + }); + + describe(@"When an accessory disconnects while a data session is open", ^{ + beforeEach(^{ + transport.controlSession = nil; + transport.session = [[SDLIAPSession alloc] initWithAccessory:mockAccessory forProtocol:@"com.smartdevicelink.multisession"]; + transport.accessoryConnectDuringActiveSession = YES; + NSNotification *accessoryDisconnectedNotification = [[NSNotification alloc] initWithName:EAAccessoryDidDisconnectNotification object:nil userInfo:@{EAAccessoryKey: mockAccessory}]; + [[NSNotificationCenter defaultCenter] postNotification:accessoryDisconnectedNotification]; + }); + + it(@"It should close the open data session", ^{ + expect(transport.session).to(beNil()); + expect(transport.controlSession).to(beNil()); + expect(transport.retryCounter).to(equal(0)); + expect(transport.accessoryConnectDuringActiveSession).to(beFalse()); + expect(transport.sessionSetupInProgress).to(beFalse()); + }); + }); + + describe(@"When an accessory disconnects while a control session is open", ^{ + beforeEach(^{ + transport.controlSession = [[SDLIAPSession alloc] initWithAccessory:mockAccessory forProtocol:@"com.smartdevicelink.prot0"];; + transport.session = nil; + transport.accessoryConnectDuringActiveSession = NO; + transport.sessionSetupInProgress = YES; + transport.retryCounter = 1; + NSNotification *accessoryDisconnectedNotification = [[NSNotification alloc] initWithName:EAAccessoryDidDisconnectNotification object:nil userInfo:@{EAAccessoryKey: mockAccessory}]; + [[NSNotificationCenter defaultCenter] postNotification:accessoryDisconnectedNotification]; + }); + + it(@"It should close the open control session", ^{ + expect(transport.session).to(beNil()); + expect(transport.controlSession).to(beNil()); + expect(transport.retryCounter).to(equal(0)); + expect(transport.accessoryConnectDuringActiveSession).to(beFalse()); + expect(transport.sessionSetupInProgress).to(beFalse()); + }); + }); +}); + +QuickSpecEnd + |