summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNicoleYarroch <nicole@livio.io>2019-02-21 15:57:09 -0500
committerNicoleYarroch <nicole@livio.io>2019-02-21 15:57:09 -0500
commitf1f2d968630266cf83cbbc4751cddb7e5f358394 (patch)
treecc003494328c332c24d587cac9a58e3851ca8381
parent0d30a06df3901482dbffa4be665211ee98133383 (diff)
parent6920985a50b8eebb4d919a2589d47250964daa37 (diff)
downloadsdl_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.pbxproj30
-rw-r--r--SmartDeviceLink/SDLIAPSession.m46
-rw-r--r--SmartDeviceLink/SDLIAPTransport.h4
-rw-r--r--SmartDeviceLink/SDLIAPTransport.m84
-rw-r--r--SmartDeviceLink/SDLLifecycleManager.m4
-rw-r--r--SmartDeviceLinkTests/TransportSpecs/TCP/SDLTCPTransportSpec.m (renamed from SmartDeviceLinkTests/TransportSpecs/SDLTCPTransportSpec.m)0
-rw-r--r--SmartDeviceLinkTests/TransportSpecs/iAP/EAAccessory+OCMock.m62
-rw-r--r--SmartDeviceLinkTests/TransportSpecs/iAP/SDLIAPSessionSpec.m165
-rw-r--r--SmartDeviceLinkTests/TransportSpecs/iAP/SDLIAPTransportSpec.m118
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
+