summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Fischer <joeljfischer@gmail.com>2020-04-15 13:25:12 -0400
committerGitHub <noreply@github.com>2020-04-15 13:25:12 -0400
commit5e01d37e46c359ee0398963ac0d64d426bbb21e0 (patch)
treee936c2edff629b66b91baa94e2d32e770479f337
parent765a84553d5f63f178a096ae9ea2374bca461df3 (diff)
parent40a63c09f19f88c6f512a242478f918cfaa0ad9d (diff)
downloadsdl_ios-5e01d37e46c359ee0398963ac0d64d426bbb21e0.tar.gz
Merge pull request #1617 from smartdevicelink/bugfix/issue_1560_delay_shutting_down_secondary_transport
Delay shutting down secondary transport when device app is backgrounded
-rw-r--r--SmartDeviceLink/SDLBackgroundTaskManager.h15
-rw-r--r--SmartDeviceLink/SDLBackgroundTaskManager.m32
-rw-r--r--SmartDeviceLink/SDLSecondaryTransportManager.m47
-rw-r--r--SmartDeviceLink/SDLStreamingAudioLifecycleManager.m14
-rw-r--r--SmartDeviceLink/SDLStreamingVideoLifecycleManager.m14
-rw-r--r--SmartDeviceLinkTests/DevAPISpecs/SDLStreamingAudioLifecycleManagerSpec.m34
-rw-r--r--SmartDeviceLinkTests/DevAPISpecs/SDLStreamingVideoLifecycleManagerSpec.m22
-rw-r--r--SmartDeviceLinkTests/ProxySpecs/SDLSecondaryTransportManagerSpec.m187
8 files changed, 291 insertions, 74 deletions
diff --git a/SmartDeviceLink/SDLBackgroundTaskManager.h b/SmartDeviceLink/SDLBackgroundTaskManager.h
index 0732b6fda..8d9f18ad1 100644
--- a/SmartDeviceLink/SDLBackgroundTaskManager.h
+++ b/SmartDeviceLink/SDLBackgroundTaskManager.h
@@ -16,6 +16,10 @@ NS_ASSUME_NONNULL_BEGIN
*/
@interface SDLBackgroundTaskManager : NSObject
+/// Handler called when the background task is about to expire. Use this handler to perform some cleanup before the background task is destroyed. When you have finished cleanup, you must call the `endBackgroundTask` function so the background task can be destroyed. If you do not call `endBackgroundTask`, the system may kill the app.
+/// @return Whether or not to wait for the subscriber to cleanup. If NO, the background task will be killed immediately. If YES, the background task will not be destroyed until the `endBackgroundTask` method is called by the subscriber.
+@property (copy, nonatomic, nullable) BOOL (^taskExpiringHandler)(void);
+
- (instancetype)init NS_UNAVAILABLE;
/**
@@ -26,17 +30,10 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (instancetype)initWithBackgroundTaskName:(NSString *)backgroundTaskName;
-/**
- * Starts a background task that allows the app to establish a session while app is backgrounded. If the app is not currently backgrounded, the background task will remain dormant until the app moves to the background.
- */
+/// Starts a background task. If the app is not currently backgrounded, the background task will remain dormant until the app moves to the background.
- (void)startBackgroundTask;
-/**
- * Cleans up a background task when it is stopped. This should be called when:
- *
- * 1. The app has established a session.
- * 2. The system has called the `expirationHandler` for the background task. The system may kill the app if the background task is not ended when `expirationHandler` is called.
- */
+/// Destroys the background task.
- (void)endBackgroundTask;
@end
diff --git a/SmartDeviceLink/SDLBackgroundTaskManager.m b/SmartDeviceLink/SDLBackgroundTaskManager.m
index 2109a40cd..7746008cf 100644
--- a/SmartDeviceLink/SDLBackgroundTaskManager.m
+++ b/SmartDeviceLink/SDLBackgroundTaskManager.m
@@ -22,7 +22,7 @@ NS_ASSUME_NONNULL_BEGIN
@implementation SDLBackgroundTaskManager
- (instancetype)initWithBackgroundTaskName:(NSString *)backgroundTaskName {
- SDLLogV(@"SDLBackgroundTaskManager init with name %@", backgroundTaskName);
+ SDLLogV(@"Creating background task manager with name %@", backgroundTaskName);
self = [super init];
if (!self) {
return nil;
@@ -39,27 +39,47 @@ NS_ASSUME_NONNULL_BEGIN
return;
}
- __weak typeof(self) weakself = self;
+ __weak typeof(self) weakSelf = self;
self.currentBackgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithName:self.backgroundTaskName expirationHandler:^{
- SDLLogD(@"The %@ background task expired", self.backgroundTaskName);
- [weakself endBackgroundTask];
+ __strong typeof(weakSelf) strongSelf = weakSelf;
+ SDLLogD(@"The background task %@ is expiring.", strongSelf.backgroundTaskName);
+
+ // We have ~1 second to do cleanup before ending the background task. If we take too long, the system will kill the app.
+ if (strongSelf.taskExpiringHandler != nil) {
+ SDLLogD(@"Checking if subscriber wants to to perform some cleanup before ending the background task %@", strongSelf.backgroundTaskName);
+ BOOL waitForCleanupToFinish = strongSelf.taskExpiringHandler();
+ if (waitForCleanupToFinish) {
+ SDLLogD(@"The subscriber will end background task itself %@. Waiting...", self.backgroundTaskName);
+ } else {
+ SDLLogV(@"Subscriber does not want to perform cleanup. Ending the background task %@", strongSelf.backgroundTaskName);
+ [strongSelf endBackgroundTask];
+ }
+ } else {
+ // No subscriber. Just end the background task.
+ SDLLogV(@"Ending background task %@", strongSelf.backgroundTaskName);
+ [strongSelf endBackgroundTask];
+ }
}];
SDLLogD(@"The %@ background task started with id: %lu", self.backgroundTaskName, (unsigned long)self.currentBackgroundTaskId);
}
- (void)endBackgroundTask {
+ SDLLogV(@"Attempting to end background task %@", self.backgroundTaskName);
+ self.taskExpiringHandler = nil;
+
if (self.currentBackgroundTaskId == UIBackgroundTaskInvalid) {
- SDLLogV(@"Background task already ended. Returning...");
+ SDLLogV(@"Background task %@ with id %lu already ended. Returning...", self.backgroundTaskName, (unsigned long)self.currentBackgroundTaskId);
return;
}
- SDLLogD(@"Ending background task with id: %lu", (unsigned long)self.currentBackgroundTaskId);
+ SDLLogD(@"Ending background task %@ with id: %lu", self.backgroundTaskName, (unsigned long)self.currentBackgroundTaskId);
[[UIApplication sharedApplication] endBackgroundTask:self.currentBackgroundTaskId];
self.currentBackgroundTaskId = UIBackgroundTaskInvalid;
}
- (void)dealloc {
+ SDLLogV(@"Destroying the manager");
[self endBackgroundTask];
}
diff --git a/SmartDeviceLink/SDLSecondaryTransportManager.m b/SmartDeviceLink/SDLSecondaryTransportManager.m
index ad2dc4659..a65bd4bb3 100644
--- a/SmartDeviceLink/SDLSecondaryTransportManager.m
+++ b/SmartDeviceLink/SDLSecondaryTransportManager.m
@@ -111,7 +111,7 @@ struct TransportProtocolUpdated {
@property (strong, nonatomic, nullable) SDLHMILevel currentHMILevel;
/// A background task used to close the secondary transport before the app is suspended.
-@property (copy, nonatomic) SDLBackgroundTaskManager *backgroundTaskManager;
+@property (strong, nonatomic) SDLBackgroundTaskManager *backgroundTaskManager;
@end
@@ -300,12 +300,12 @@ struct TransportProtocolUpdated {
}
- (void)willTransitionFromStateRegisteredToStateConfigured {
- SDLLogD(@"Configuring: stopping services on secondary transport");
+ SDLLogD(@"Manger is closing transport but is configured to resume the secondary transport. Stopping services on secondary transport");
[self sdl_handleTransportUpdateWithPrimaryAvailable:YES secondaryAvailable:NO];
}
- (void)willTransitionFromStateRegisteredToStateReconnecting {
- SDLLogD(@"Reconnecting: stopping services on secondary transport");
+ SDLLogD(@"Manger is closing transport but will try to reconnect if configured correctly. Stopping services on secondary transport");
[self sdl_handleTransportUpdateWithPrimaryAvailable:YES secondaryAvailable:NO];
}
@@ -682,29 +682,56 @@ struct TransportProtocolUpdated {
dispatch_async(self.stateMachineQueue, ^{
__strong typeof(self) strongSelf = weakSelf;
if (notification.name == UIApplicationWillResignActiveNotification) {
+ SDLLogD(@"App will enter the background");
if ([strongSelf sdl_isTransportOpened] && strongSelf.secondaryTransportType == SDLSecondaryTransportTypeTCP) {
- SDLLogD(@"Disconnecting TCP transport since the app will go to background");
- // Start a background task so we can tear down the TCP socket successfully before the app is suspended
+ SDLLogD(@"Starting background task to keep TCP transport alive");
+ strongSelf.backgroundTaskManager.taskExpiringHandler = [strongSelf sdl_backgroundTaskEndedHandler];
[strongSelf.backgroundTaskManager startBackgroundTask];
- [strongSelf.stateMachine transitionToState:SDLSecondaryTransportStateConfigured];
} else {
- SDLLogD(@"App will go to background. TCP transport already disconnected: %@", strongSelf.stateMachine.currentState);
+ SDLLogD(@"TCP transport already disconnected, will not start a background task.");
}
} else if (notification.name == UIApplicationDidBecomeActiveNotification) {
- if ([strongSelf.stateMachine isCurrentState:SDLSecondaryTransportStateConfigured]
+ SDLLogD(@"App entered the foreground");
+ if ([strongSelf.stateMachine isCurrentState:SDLSecondaryTransportStateRegistered]) {
+ SDLLogD(@"In the registered state; TCP transport has not yet been shutdown. Ending the background task.");
+ [strongSelf.backgroundTaskManager endBackgroundTask];
+ } else if ([strongSelf.stateMachine isCurrentState:SDLSecondaryTransportStateConfigured]
&& strongSelf.secondaryTransportType == SDLSecondaryTransportTypeTCP
&& [strongSelf sdl_isTCPReady]
&& [strongSelf sdl_isHMILevelNonNone]) {
- SDLLogD(@"Resuming TCP transport because the app came into the foreground");
+ SDLLogD(@"In the configured state; restarting the TCP transport. Ending the background task.");
[strongSelf.backgroundTaskManager endBackgroundTask];
[strongSelf.stateMachine transitionToState:SDLSecondaryTransportStateConnecting];
} else {
- SDLLogD(@"App returning to foreground. TCP transport not ready to connect: %@", strongSelf.stateMachine.currentState);
+ SDLLogD(@"TCP transport not ready to start, our current state is: %@", strongSelf.stateMachine.currentState);
}
}
});
}
+/// Handles a notification that the background task is about to expire. If the app is still backgrounded we must close the TCP socket, which can take a few moments to complete. When this manager transitons to the Configured state, the `SDLStreamingMediaManager` is notified that the secondary transport wants to shutdown via the `streamingProtocolDelegate`. The `SDLStreamingMediaManager` sends an end video service control frame and an end audio service control frame and waits for responses to both requests from the module. Once the module has responded to both end service requests, the `SDLStreamingMediaManager` notifies us that the TCP socket can be shutdown by calling the `disconnectSecondaryTransport` method. Finally, once we know the socket has shutdown, we can end the background task. To ensure that all the shutdown steps are performed, we must delay shutting down the background task, otherwise some of the steps might not complete due to the app being suspended. Improper shutdown can cause trouble when establishing a new streaming session as either the new TCP connection will fail (due to the TCP socket's I/O streams not shutting down) or restarting the video and audio streams can fail (due to Core not receiving the end service requests). On the other hand, we can end the background task immediately if the app has re-entered the foreground or the manager has shutdown as no cleanup needs to be performed.
+/// @return A background task ended handler
+- (nullable BOOL (^)(void))sdl_backgroundTaskEndedHandler {
+ __weak typeof(self) weakSelf = self;
+ return ^{
+ __strong typeof(self) strongSelf = weakSelf;
+ if (strongSelf.sdl_getAppState == UIApplicationStateActive || [strongSelf.stateMachine isCurrentState:SDLSecondaryTransportStateStopped]) {
+ // Return NO as we do not need to perform any cleanup and can end the background task immediately
+ SDLLogV(@"No cleanup needed since app has been foregrounded.");
+ return NO;
+ } else if ([strongSelf.stateMachine isCurrentState:SDLSecondaryTransportStateStopped]) {
+ // Return NO as we do not need to perform any cleanup and can end the background task immediately
+ SDLLogV(@"No cleanup needed since manager has been stopped.");
+ return NO;
+ } else {
+ // Return YES as we want to delay ending the background task until shutdown of the secondary transport has finished. Transitoning to the Configured state starts the process of shutting down the streaming services and the TCP socket which can take a few moments to complete. Once the streaming services have shutdown, the `SDLStreamingMediaManager` calls the `disconnectSecondaryTransport` method. The `disconnectSecondaryTransport` takes care of destroying the background task after disconnecting the TCP transport.
+ SDLLogD(@"Performing cleanup due to the background task expiring: disconnecting the TCP transport.");
+ [strongSelf.stateMachine transitionToState:SDLSecondaryTransportStateConfigured];
+ return YES;
+ }
+ };
+}
+
#pragma mark - Utility methods
- (SDLSecondaryTransportType)sdl_convertTransportType:(NSString *)transportString {
diff --git a/SmartDeviceLink/SDLStreamingAudioLifecycleManager.m b/SmartDeviceLink/SDLStreamingAudioLifecycleManager.m
index 36e0f3920..50fb56dac 100644
--- a/SmartDeviceLink/SDLStreamingAudioLifecycleManager.m
+++ b/SmartDeviceLink/SDLStreamingAudioLifecycleManager.m
@@ -155,6 +155,11 @@ NS_ASSUME_NONNULL_BEGIN
_audioEncrypted = NO;
[[NSNotificationCenter defaultCenter] postNotificationName:SDLAudioStreamDidStopNotification object:nil];
+
+ if (self.audioServiceEndedCompletionHandler != nil) {
+ self.audioServiceEndedCompletionHandler();
+ self.audioServiceEndedCompletionHandler = nil;
+ }
}
- (void)didEnterStateAudioStreamStarting {
@@ -210,12 +215,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)handleProtocolEndServiceACKMessage:(SDLProtocolMessage *)endServiceACK {
if (endServiceACK.header.serviceType != SDLServiceTypeAudio) { return; }
-
SDLLogD(@"Request to end audio service ACKed");
- if (self.audioServiceEndedCompletionHandler != nil) {
- self.audioServiceEndedCompletionHandler();
- self.audioServiceEndedCompletionHandler = nil;
- }
[self.audioStreamStateMachine transitionToState:SDLAudioStreamManagerStateStopped];
}
@@ -225,10 +225,6 @@ NS_ASSUME_NONNULL_BEGIN
SDLControlFramePayloadNak *nakPayload = [[SDLControlFramePayloadNak alloc] initWithData:endServiceNAK.payload];
SDLLogE(@"Request to end audio service NAKed with playlod: %@", nakPayload);
- if (self.audioServiceEndedCompletionHandler != nil) {
- self.audioServiceEndedCompletionHandler();
- self.audioServiceEndedCompletionHandler = nil;
- }
/// Core will NAK the audio end service control frame if audio is not streaming or if video is streaming but the HMI does not recognize that audio is streaming.
[self.audioStreamStateMachine transitionToState:SDLAudioStreamManagerStateStopped];
diff --git a/SmartDeviceLink/SDLStreamingVideoLifecycleManager.m b/SmartDeviceLink/SDLStreamingVideoLifecycleManager.m
index bdfa9968a..be9135cb8 100644
--- a/SmartDeviceLink/SDLStreamingVideoLifecycleManager.m
+++ b/SmartDeviceLink/SDLStreamingVideoLifecycleManager.m
@@ -364,6 +364,11 @@ typedef void(^SDLVideoCapabilityResponseHandler)(SDLVideoStreamingCapability *_N
[self sdl_disposeDisplayLink];
[[NSNotificationCenter defaultCenter] postNotificationName:SDLVideoStreamDidStopNotification object:nil];
+
+ if (self.videoServiceEndedCompletionHandler != nil) {
+ self.videoServiceEndedCompletionHandler();
+ self.videoServiceEndedCompletionHandler = nil;
+ }
}
- (void)didEnterStateVideoStreamStarting {
@@ -560,12 +565,7 @@ typedef void(^SDLVideoCapabilityResponseHandler)(SDLVideoStreamingCapability *_N
- (void)handleProtocolEndServiceACKMessage:(SDLProtocolMessage *)endServiceACK {
if (endServiceACK.header.serviceType != SDLServiceTypeVideo) { return; }
-
SDLLogD(@"Request to end video service ACKed");
- if (self.videoServiceEndedCompletionHandler != nil) {
- self.videoServiceEndedCompletionHandler();
- self.videoServiceEndedCompletionHandler = nil;
- }
[self.videoStreamStateMachine transitionToState:SDLVideoStreamManagerStateStopped];
}
@@ -575,10 +575,6 @@ typedef void(^SDLVideoCapabilityResponseHandler)(SDLVideoStreamingCapability *_N
SDLControlFramePayloadNak *nakPayload = [[SDLControlFramePayloadNak alloc] initWithData:endServiceNAK.payload];
SDLLogE(@"Request to end video service NAKed with payload: %@", nakPayload);
- if (self.videoServiceEndedCompletionHandler != nil) {
- self.videoServiceEndedCompletionHandler();
- self.videoServiceEndedCompletionHandler = nil;
- }
/// Core will NAK the video end service control frame if video is not streaming or if video is streaming but the HMI does not recognize that video is streaming.
[self.videoStreamStateMachine transitionToState:SDLVideoStreamManagerStateStopped];
diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLStreamingAudioLifecycleManagerSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLStreamingAudioLifecycleManagerSpec.m
index 186be93c8..4770f44f1 100644
--- a/SmartDeviceLinkTests/DevAPISpecs/SDLStreamingAudioLifecycleManagerSpec.m
+++ b/SmartDeviceLinkTests/DevAPISpecs/SDLStreamingAudioLifecycleManagerSpec.m
@@ -392,6 +392,16 @@ describe(@"the streaming audio manager", ^{
});
describe(@"attempting to stop the manager", ^{
+ __block BOOL handlerCalled = nil;
+
+ beforeEach(^{
+ handlerCalled = NO;
+ [streamingLifecycleManager endAudioServiceWithCompletionHandler:^ {
+ handlerCalled = YES;
+ }];
+ streamingLifecycleManager.connectedVehicleMake = @"OEM_make_2";
+ });
+
context(@"when the manager is READY", ^{
beforeEach(^{
[streamingLifecycleManager.audioStreamStateMachine setToState:SDLAudioStreamManagerStateReady fromOldState:nil callEnterTransition:NO];
@@ -405,48 +415,44 @@ describe(@"the streaming audio manager", ^{
expect(streamingLifecycleManager.hmiLevel).to(equal(SDLHMILevelNone));
expect(streamingLifecycleManager.connectedVehicleMake).to(beNil());
OCMVerify([mockAudioStreamManager stop]);
-
+ expect(handlerCalled).to(beTrue());
});
});
- context(@"when the manager is STOPPED", ^{
+ context(@"when the manager is already stopped", ^{
beforeEach(^{
[streamingLifecycleManager.audioStreamStateMachine setToState:SDLAudioStreamManagerStateStopped fromOldState:nil callEnterTransition:NO];
[streamingLifecycleManager stop];
});
- it(@"should stay in the stopped state and reset the saved properties", ^{
+ it(@"should stay in the stopped state", ^{
expect(streamingLifecycleManager.currentAudioStreamState).to(equal(SDLAudioStreamManagerStateStopped));
expect(streamingLifecycleManager.protocol).to(beNil());
expect(streamingLifecycleManager.hmiLevel).to(equal(SDLHMILevelNone));
expect(streamingLifecycleManager.connectedVehicleMake).to(beNil());
- OCMVerify([mockAudioStreamManager stop]);
+ OCMReject([mockAudioStreamManager stop]);
+ expect(handlerCalled).to(beFalse());
});
});
});
describe(@"starting the manager when it's STOPPED", ^{
__block SDLProtocol *protocolMock = OCMClassMock([SDLProtocol class]);
- __block BOOL handlerCalled = nil;
beforeEach(^{
- handlerCalled = NO;
[streamingLifecycleManager startWithProtocol:protocolMock];
+ [streamingLifecycleManager endAudioServiceWithCompletionHandler:^{}];
});
context(@"when stopping the audio service due to a secondary transport shutdown", ^{
beforeEach(^{
[streamingLifecycleManager.audioStreamStateMachine setToState:SDLAudioStreamManagerStateReady fromOldState:nil callEnterTransition:NO];
- [streamingLifecycleManager endAudioServiceWithCompletionHandler:^ {
- handlerCalled = YES;
- }];
});
it(@"should reset the audio stream manger and send an end audio service control frame", ^{
OCMVerify([mockAudioStreamManager stop]);
OCMVerify([protocolMock endServiceWithType:SDLServiceTypeAudio]);
-
});
context(@"when the end audio service ACKs", ^{
@@ -464,8 +470,8 @@ describe(@"the streaming audio manager", ^{
[streamingLifecycleManager handleProtocolEndServiceACKMessage:testAudioMessage];
});
- it(@"should call the handler", ^{
- expect(handlerCalled).to(beTrue());
+ it(@"should transistion to the stopped state", ^{
+ expect(streamingLifecycleManager.currentAudioStreamState).to(equal(SDLAudioStreamManagerStateStopped));
});
});
@@ -484,8 +490,8 @@ describe(@"the streaming audio manager", ^{
[streamingLifecycleManager handleProtocolEndServiceNAKMessage:testAudioMessage];
});
- it(@"should call the handler", ^{
- expect(handlerCalled).to(beTrue());
+ it(@"should transistion to the stopped state", ^{
+ expect(streamingLifecycleManager.currentAudioStreamState).to(equal(SDLAudioStreamManagerStateStopped));
});
});
});
diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLStreamingVideoLifecycleManagerSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLStreamingVideoLifecycleManagerSpec.m
index 850f598a1..4dfae005b 100644
--- a/SmartDeviceLinkTests/DevAPISpecs/SDLStreamingVideoLifecycleManagerSpec.m
+++ b/SmartDeviceLinkTests/DevAPISpecs/SDLStreamingVideoLifecycleManagerSpec.m
@@ -766,7 +766,13 @@ describe(@"the streaming video manager", ^{
});
describe(@"stopping the manager", ^{
+ __block BOOL handlerCalled = nil;
+
beforeEach(^{
+ handlerCalled = NO;
+ [streamingLifecycleManager endVideoServiceWithCompletionHandler:^ {
+ handlerCalled = YES;
+ }];
streamingLifecycleManager.connectedVehicleMake = @"OEM_make_2";
});
@@ -784,6 +790,7 @@ describe(@"the streaming video manager", ^{
expect(streamingLifecycleManager.videoStreamingState).to(equal(SDLVideoStreamingStateNotStreamable));
expect(streamingLifecycleManager.preferredFormatIndex).to(equal(0));
expect(streamingLifecycleManager.preferredResolutionIndex).to(equal(0));
+ expect(handlerCalled).to(beTrue());
});
});
@@ -801,24 +808,21 @@ describe(@"the streaming video manager", ^{
expect(streamingLifecycleManager.videoStreamingState).to(equal(SDLVideoStreamingStateNotStreamable));
expect(streamingLifecycleManager.preferredFormatIndex).to(equal(0));
expect(streamingLifecycleManager.preferredResolutionIndex).to(equal(0));
+ expect(handlerCalled).to(beFalse());
});
});
});
describe(@"starting the manager", ^{
__block SDLProtocol *protocolMock = OCMClassMock([SDLProtocol class]);
- __block BOOL handlerCalled = nil;
beforeEach(^{
- handlerCalled = NO;
[streamingLifecycleManager startWithProtocol:protocolMock];
});
describe(@"then ending the video service through the secondary transport", ^{
beforeEach(^{
- [streamingLifecycleManager endVideoServiceWithCompletionHandler:^ {
- handlerCalled = YES;
- }];
+ [streamingLifecycleManager endVideoServiceWithCompletionHandler:^{}];
});
it(@"should send an end video service control frame", ^{
@@ -840,8 +844,8 @@ describe(@"the streaming video manager", ^{
[streamingLifecycleManager handleProtocolEndServiceACKMessage:testVideoMessage];
});
- it(@"should call the handler", ^{
- expect(handlerCalled).to(beTrue());
+ it(@"should transistion to the stopped state", ^{
+ expect(streamingLifecycleManager.currentVideoStreamState).to(equal(SDLVideoStreamManagerStateStopped));
});
});
@@ -860,8 +864,8 @@ describe(@"the streaming video manager", ^{
[streamingLifecycleManager handleProtocolEndServiceNAKMessage:testVideoMessage];
});
- it(@"should call the handler", ^{
- expect(handlerCalled).to(beTrue());
+ it(@"should transistion to the stopped state", ^{
+ expect(streamingLifecycleManager.currentVideoStreamState).to(equal(SDLVideoStreamManagerStateStopped));
});
});
});
diff --git a/SmartDeviceLinkTests/ProxySpecs/SDLSecondaryTransportManagerSpec.m b/SmartDeviceLinkTests/ProxySpecs/SDLSecondaryTransportManagerSpec.m
index 04d836054..1ca8a23c7 100644
--- a/SmartDeviceLinkTests/ProxySpecs/SDLSecondaryTransportManagerSpec.m
+++ b/SmartDeviceLinkTests/ProxySpecs/SDLSecondaryTransportManagerSpec.m
@@ -10,6 +10,7 @@
#import <Nimble/Nimble.h>
#import <OCMock/OCMock.h>
+#import "SDLBackgroundTaskManager.h"
#import "SDLControlFramePayloadRegisterSecondaryTransportNak.h"
#import "SDLControlFramePayloadRPCStartServiceAck.h"
#import "SDLControlFramePayloadTransportEventUpdate.h"
@@ -61,27 +62,45 @@ static const int TCPPortUnspecified = -1;
@property (strong, nonatomic, nullable) NSString *ipAddress;
@property (assign, nonatomic) int tcpPort;
@property (strong, nonatomic, nullable) SDLHMILevel currentHMILevel;
+@property (strong, nonatomic) SDLBackgroundTaskManager *backgroundTaskManager;
+
+- (nullable BOOL (^)(void))sdl_backgroundTaskEndedHandler;
@end
@interface SDLSecondaryTransportManager (ForTest)
// Swap sdl_getAppState method to dummy implementation.
-// Since the test runs on the main thread, dispatch_sync()-ing to the main thread doesn't work.
-+ (void)swapGetAppStateMethod;
+// Since the test runs on the main thread, dispatch_sync()-ing to the main thread freezes the tests.
++ (void)swapGetActiveAppStateMethod;
++ (void)swapGetInactiveAppStateMethod;
@end
@implementation SDLSecondaryTransportManager (ForTest)
-- (UIApplicationState)dummyGetAppState {
- NSLog(@"Testing: app state for secondary transport manager is always ACTIVE");
+
+- (UIApplicationState)dummyGetActiveAppState {
+ NSLog(@"Testing: app state for secondary transport manager is ACTIVE");
return UIApplicationStateActive;
}
-+ (void)swapGetAppStateMethod {
++ (void)swapGetActiveAppStateMethod {
SEL selector = NSSelectorFromString(@"sdl_getAppState");
Method from = class_getInstanceMethod(self, selector);
- Method to = class_getInstanceMethod(self, @selector(dummyGetAppState));
+ Method to = class_getInstanceMethod(self, @selector(dummyGetActiveAppState));
method_exchangeImplementations(from, to);
}
+
+- (UIApplicationState)dummyGetInactiveAppState {
+ NSLog(@"Testing: app state for secondary transport manager is INACTIVE");
+ return UIApplicationStateBackground;
+}
+
++ (void)swapGetInactiveAppStateMethod {
+ SEL selector = NSSelectorFromString(@"sdl_getAppState");
+ Method from = class_getInstanceMethod(self, selector);
+ Method to = class_getInstanceMethod(self, @selector(dummyGetInactiveAppState));
+ method_exchangeImplementations(from, to);
+}
+
@end
@interface SDLTCPTransport (ConnectionDisabled)
@@ -152,7 +171,7 @@ describe(@"the secondary transport manager ", ^{
};
beforeEach(^{
- [SDLSecondaryTransportManager swapGetAppStateMethod];
+ [SDLSecondaryTransportManager swapGetActiveAppStateMethod];
[SDLTCPTransport swapConnectionMethods];
[SDLIAPTransport swapConnectionMethods];
@@ -174,7 +193,7 @@ describe(@"the secondary transport manager ", ^{
[SDLIAPTransport swapConnectionMethods];
[SDLTCPTransport swapConnectionMethods];
- [SDLSecondaryTransportManager swapGetAppStateMethod];
+ [SDLSecondaryTransportManager swapGetActiveAppStateMethod];
});
@@ -1093,6 +1112,158 @@ describe(@"the secondary transport manager ", ^{
});
});
});
+
+ describe(@"app lifecycle state change", ^{
+ __block SDLBackgroundTaskManager *mockBackgroundTaskManager = nil;
+
+ beforeEach(^{
+ // In the tests, we assume primary transport is iAP
+ testPrimaryProtocol = [[SDLProtocol alloc] init];
+ testPrimaryTransport = [[SDLIAPTransport alloc] init];
+ testPrimaryProtocol.transport = testPrimaryTransport;
+
+ dispatch_sync(testStateMachineQueue, ^{
+ [manager startWithPrimaryProtocol:testPrimaryProtocol];
+ });
+
+ mockBackgroundTaskManager = OCMPartialMock([[SDLBackgroundTaskManager alloc] initWithBackgroundTaskName:@"com.test.backgroundTask"]);
+ manager.backgroundTaskManager = mockBackgroundTaskManager;
+ });
+
+ context(@"app enters the background", ^{
+ beforeEach(^{
+ manager.secondaryTransportType = SDLTransportSelectionTCP;
+ });
+
+ describe(@"if the secondary transport is connected", ^{
+ beforeEach(^{
+ [manager.stateMachine setToState:SDLSecondaryTransportStateRegistered fromOldState:nil callEnterTransition:NO];
+
+ [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationWillResignActiveNotification object:nil];
+
+ // Wait for the notification to propagate
+ [NSThread sleepForTimeInterval:0.1];
+ });
+
+ it(@"should start a background task and stay connected", ^{
+ OCMVerify([mockBackgroundTaskManager startBackgroundTask]);
+ expect(manager.stateMachine.currentState).to(equal(SDLSecondaryTransportStateRegistered));
+ });
+ });
+
+ describe(@"if the secondary transport has not yet connected", ^{
+ beforeEach(^{
+ [manager.stateMachine setToState:SDLSecondaryTransportStateConfigured fromOldState:nil callEnterTransition:NO];
+
+ [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationWillResignActiveNotification object:nil];
+
+ // Wait for the notification to propagate
+ [NSThread sleepForTimeInterval:0.1];
+ });
+
+ it(@"should ignore the state change notification", ^{
+ OCMReject([mockBackgroundTaskManager startBackgroundTask]);
+ expect(manager.stateMachine.currentState).to(equal(SDLSecondaryTransportStateConfigured));
+ });
+ });
+ });
+
+ context(@"app enters the foreground", ^{
+ describe(@"if the secondary transport is still connected", ^{
+ beforeEach(^{
+ [manager.stateMachine setToState:SDLSecondaryTransportStateRegistered fromOldState:nil callEnterTransition:NO];
+
+ [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationDidBecomeActiveNotification object:nil];
+
+ // Wait for the notification to propagate
+ [NSThread sleepForTimeInterval:0.1];
+ });
+
+ it(@"should end the background task and stay in the connected state", ^{
+ OCMVerify([mockBackgroundTaskManager endBackgroundTask]);
+ expect(manager.stateMachine.currentState).to(equal(SDLSecondaryTransportStateRegistered));
+ });
+ });
+
+ describe(@"if the secondary transport is not connected but is configured", ^{
+ beforeEach(^{
+ manager.ipAddress = @"192.555.23.1";
+ manager.tcpPort = 54321;
+ manager.currentHMILevel = SDLHMILevelFull;
+
+ manager.secondaryTransportType = SDLTransportSelectionTCP;
+ [manager.stateMachine setToState:SDLSecondaryTransportStateConfigured fromOldState:nil callEnterTransition:NO];
+
+ [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationDidBecomeActiveNotification object:nil];
+
+ // Wait for the notification to propagate
+ [NSThread sleepForTimeInterval:0.1];
+ });
+
+ it(@"should end the background task and try to restart the TCP transport", ^{
+ OCMVerify([mockBackgroundTaskManager endBackgroundTask]);
+ expect(manager.stateMachine.currentState).to(equal(SDLSecondaryTransportStateConnecting));
+ });
+ });
+
+ describe(@"if the secondary transport not connected and is not configured", ^{
+ beforeEach(^{
+ [manager.stateMachine setToState:SDLSecondaryTransportStateConnecting fromOldState:nil callEnterTransition:NO];
+
+ [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationDidBecomeActiveNotification object:nil];
+
+ // Wait for the notification to propagate
+ [NSThread sleepForTimeInterval:0.1];
+ });
+
+ it(@"should ignore the state change notification", ^{
+ OCMReject([mockBackgroundTaskManager endBackgroundTask]);
+ expect(manager.stateMachine.currentState).to(equal(SDLSecondaryTransportStateConnecting));
+ });
+ });
+ });
+
+ describe(@"When the background task expires", ^{
+ context(@"If the app is still in the background", ^{
+ beforeEach(^{
+ [SDLSecondaryTransportManager swapGetInactiveAppStateMethod];
+ });
+
+ it(@"should stop the TCP transport if the app is still in the background and perform cleanup before ending the background task", ^{
+ [manager.stateMachine setToState:SDLSecondaryTransportStateRegistered fromOldState:nil callEnterTransition:NO];
+
+ BOOL waitForCleanupToFinish = manager.sdl_backgroundTaskEndedHandler();
+
+ expect(manager.stateMachine.currentState).to(equal(SDLSecondaryTransportStateConfigured));
+ expect(waitForCleanupToFinish).to(beTrue());
+ });
+
+ it(@"should ignore the notification if the manager has stopped before the background task ended and immediately end the background task", ^{
+ [manager.stateMachine setToState:SDLSecondaryTransportStateStopped fromOldState:nil callEnterTransition:NO];
+
+ BOOL waitForCleanupToFinish = manager.sdl_backgroundTaskEndedHandler();
+
+ expect(manager.stateMachine.currentState).to(equal(SDLSecondaryTransportStateStopped));
+ expect(waitForCleanupToFinish).to(beFalse());
+ });
+
+ afterEach(^{
+ [SDLSecondaryTransportManager swapGetInactiveAppStateMethod];
+ });
+ });
+
+ context(@"If the app is has entered the foreground", ^{
+ it(@"should ignore the notification if the app has returned to the foreground and immediately end the background task", ^{
+ [manager.stateMachine setToState:SDLSecondaryTransportStateRegistered fromOldState:nil callEnterTransition:NO];
+
+ BOOL waitForCleanupToFinish = manager.sdl_backgroundTaskEndedHandler();
+
+ expect(manager.stateMachine.currentState).to(equal(SDLSecondaryTransportStateRegistered));
+ expect(waitForCleanupToFinish).to(beFalse());
+ });
+ });
+ });
+ });
});
QuickSpecEnd