Skip to content

Commit

Permalink
feat: Switch workspaces without restarting app
Browse files Browse the repository at this point in the history
  • Loading branch information
einsteinx2 committed Feb 28, 2024
1 parent d604e59 commit 3c3890d
Show file tree
Hide file tree
Showing 14 changed files with 361 additions and 30 deletions.
2 changes: 1 addition & 1 deletion UnitTests/MPKitSecondTestClass.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ + (nonnull NSNumber *)kitCode {
return @314;
}

- (void)deinit {
- (void)stop {

}

Expand Down
2 changes: 1 addition & 1 deletion UnitTests/MPKitSecondTestClassNoStartImmediately.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ + (nonnull NSNumber *)kitCode {
return @314;
}

- (void)deinit {
- (void)stop {

}

Expand Down
2 changes: 1 addition & 1 deletion UnitTests/MPKitTestClass.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ + (nonnull NSNumber *)kitCode {
return @42;
}

- (void)deinit {
- (void)stop {

}

Expand Down
3 changes: 3 additions & 0 deletions UnitTests/MPKitTestClassNoStartImmediately.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@
+ (nonnull NSNumber *)kitCode;

@end

@interface MPKitTestClassNoStartImmediatelyWithStop: MPKitTestClassNoStartImmediately
@end
38 changes: 27 additions & 11 deletions UnitTests/MPKitTestClassNoStartImmediately.m
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@
#import "MPKitExecStatus.h"
#import "mParticle.h"

@interface MParticle()
+ (void)executeOnMainSync:(void(^)(void))block;
@end

@interface MPKitTestClassNoStartImmediately()
@property (nonatomic, readwrite) BOOL started;
@end

@implementation MPKitTestClassNoStartImmediately

- (MPKitExecStatus *)didFinishLaunchingWithConfiguration:(NSDictionary *)configuration {
MPKitExecStatus *execStatus = nil;

_configuration = configuration;
_started = NO;
self.configuration = configuration;
self.started = NO;

execStatus = [[MPKitExecStatus alloc] initWithSDKCode:[[self class] kitCode] returnCode:MPKitReturnCodeSuccess];
return execStatus;
Expand All @@ -18,25 +26,20 @@ + (nonnull NSNumber *)kitCode {
return @42;
}

- (void)deinit {

}

- (MPKitExecStatus *)didBecomeActive {
MPKitExecStatus *execStatus = [[MPKitExecStatus alloc] initWithSDKCode:[[self class] kitCode] returnCode:MPKitReturnCodeSuccess];
return execStatus;
}

- (void)start {
_started = YES;
self.started = YES;

dispatch_async(dispatch_get_main_queue(), ^{
[MParticle executeOnMainSync:^{
NSDictionary *userInfo = @{mParticleKitInstanceKey:[[self class] kitCode]};

[[NSNotificationCenter defaultCenter] postNotificationName:mParticleKitDidBecomeActiveNotification
object:nil
userInfo:userInfo];
});
}];
}

- (MPKitExecStatus *)logBaseEvent:(MPBaseEvent *)event {
Expand Down Expand Up @@ -65,7 +68,7 @@ - (MPKitExecStatus *)logScreen:(MPEvent *)event {
}

- (id)providerKitInstance {
return _started ? self : nil;
return self.started ? self : nil;
}

- (MPKitExecStatus *)setDebugMode:(BOOL)debugMode {
Expand Down Expand Up @@ -97,3 +100,16 @@ - (nonnull MPKitExecStatus *)setUserIdentity:(nullable NSString *)identityString
}

@end

@implementation MPKitTestClassNoStartImmediatelyWithStop

+ (nonnull NSNumber *)kitCode {
return @43;
}

- (void)stop {
self.started = NO;
}

@end

4 changes: 4 additions & 0 deletions UnitTests/MPKitTestClassSideloaded.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ - (BOOL)started {
return YES;
}

- (id)providerKitInstance {
return self;
}

- (nonnull MPKitExecStatus *)didFinishLaunchingWithConfiguration:(nonnull NSDictionary *)configuration {
return [[MPKitExecStatus alloc] initWithSDKCode:self.sideloadedKitCode returnCode:MPKitReturnCodeSuccess];
}
Expand Down
176 changes: 172 additions & 4 deletions UnitTests/MParticleTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,32 @@
#import "MPIUserDefaults.h"
#import "MPURL.h"
#import "MPDevice.h"
#import "MPKitContainer.h"
#import "MPKitTestClassSideloaded.h"
#import "MPKitTestClassNoStartImmediately.h"
#import "MPKitConfiguration.h"
#import "Swift.h"
#import <AppTrackingTransparency/AppTrackingTransparency.h>

@interface MParticle ()

+ (dispatch_queue_t)messageQueue;
@property (nonatomic, strong) MPStateMachine *stateMachine;
@property (nonatomic, strong) MPBackendController *backendController;
@property (nonatomic, strong) MParticleOptions *options;
@property (nonatomic, strong, readonly) MPKitContainer *kitContainer;
- (BOOL)isValidBridgeName:(NSString *)bridgeName;
- (void)handleWebviewCommand:(NSString *)command dictionary:(NSDictionary *)dictionary;
@property (nonatomic, strong) MParticleWebView *webView;

@end

@interface MParticleUser ()
- (void)setIdentitySync:(NSString *)identityString identityType:(MPIdentity)identityType;
- (void)setUserId:(NSNumber *)userId;
@end

@interface MPKitContainer ()
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, MPKitConfiguration *> *kitConfigurations;
+ (NSMutableSet <id<MPExtensionKitProtocol>> *)kitsRegistry;
@end

@interface MParticleTests : MPBaseTestCase {
Expand All @@ -43,6 +52,9 @@ - (void)setUp {
[super setUp];
// Put setup code here. This method is called before the invocation of each test method in the class.
lastNotification = nil;

// Ensure registeredKits is empty
[MPKitContainer.kitsRegistry removeAllObjects];
}

- (void)tearDown {
Expand Down Expand Up @@ -1064,8 +1076,164 @@ - (void)testLogCrashNilStackTrace {
[mockBackend verifyWithDelay:5];
}

- (void)testLogCrashNilPlCrashReport {
// TODO: implement method to verify that logCrash is not invoked at MPBackendController
- (void)testSwitchWorkspaceOptions {
XCTestExpectation *expectation = [self expectationWithDescription:@"async work"];

MParticle *instance = [MParticle sharedInstance];
XCTAssertNotNil(instance);
XCTAssertNil(instance.options);

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
MParticleOptions *options1 = [MParticleOptions optionsWithKey:@"unit-test-key1" secret:@"unit-test-secret1"];
[instance startWithOptions:options1];
XCTAssertNotNil(instance.options);
XCTAssertEqualObjects(instance.options.apiKey, @"unit-test-key1");
XCTAssertEqualObjects(instance.options.apiSecret, @"unit-test-secret1");

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
MParticleOptions *options2 = [MParticleOptions optionsWithKey:@"unit-test-key2" secret:@"unit-test-secret2"];
[instance switchWorkspaceWithOptions:options2];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
MParticle *instance3 = [MParticle sharedInstance];
MParticle *instance4 = [MParticle sharedInstance];
XCTAssertNotNil(instance.options);
XCTAssertEqualObjects(instance.options.apiKey, @"unit-test-key1");
XCTAssertEqualObjects(instance.options.apiSecret, @"unit-test-secret1");

XCTAssertNotNil(instance3.options);
XCTAssertEqualObjects(instance3.options.apiKey, @"unit-test-key2");
XCTAssertEqualObjects(instance3.options.apiSecret, @"unit-test-secret2");
XCTAssertNotEqual(instance, instance3);
XCTAssertEqual(instance3, instance4);

[expectation fulfill];
});
});
});

[self waitForExpectationsWithTimeout:3 handler:nil];
}

- (void)testSwitchWorkspaceSideloadedKits {
XCTestExpectation *expectation = [self expectationWithDescription:@"async work"];

// Start with a sideloaded kit
MParticleOptions *options1 = [MParticleOptions optionsWithKey:@"unit-test-key" secret:@"unit-test-secret"];
MPKitTestClassSideloaded *kitTestSideloaded1 = [[MPKitTestClassSideloaded alloc] init];
options1.sideloadedKits = @[[[MPSideloadedKit alloc] initWithKitInstance:kitTestSideloaded1]];

[[MParticle sharedInstance] startWithOptions:options1];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
XCTAssertEqual(MPKitContainer.registeredKits.count, 1);
XCTAssertEqualObjects(MPKitContainer.registeredKits.anyObject.wrapperInstance, kitTestSideloaded1);

// Switch workspace with a new sideloaded kit
MParticleOptions *options2 = [MParticleOptions optionsWithKey:@"unit-test-key" secret:@"unit-test-secret"];
MPKitTestClassSideloaded *kitTestSideloaded2 = [[MPKitTestClassSideloaded alloc] init];
options2.sideloadedKits = @[[[MPSideloadedKit alloc] initWithKitInstance:kitTestSideloaded2]];

[[MParticle sharedInstance] switchWorkspaceWithOptions:options2];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
XCTAssertEqual(MPKitContainer.registeredKits.count, 1);
XCTAssertEqualObjects(MPKitContainer.registeredKits.anyObject.wrapperInstance, kitTestSideloaded2);

// Switch workspace with no sideloaded kits
MParticleOptions *options3 = [MParticleOptions optionsWithKey:@"unit-test-key" secret:@"unit-test-secret"];
[[MParticle sharedInstance] switchWorkspaceWithOptions:options3];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
XCTAssertEqual(MPKitContainer.registeredKits.count, 0);

[expectation fulfill];
});
});
});

[self waitForExpectationsWithTimeout:5 handler:nil];
}

// Kits without configurations should NOT be removed from the registry even if they implement `stop` becuase it means they weren't used by the previous workspace
- (void)testSwitchWorkspaceKitsNoConfigurations {
XCTestExpectation *expectation = [self expectationWithDescription:@"async work"];

XCTAssertEqual(MPKitContainer.registeredKits.count, 0);
[MParticle registerExtension:[[MPKitRegister alloc] initWithName:@"TestKitNoStop" className:@"MPKitTestClassNoStartImmediately"]];
[MParticle registerExtension:[[MPKitRegister alloc] initWithName:@"TestKitWithStop" className:@"MPKitTestClassNoStartImmediatelyWithStop"]];
XCTAssertEqual(MPKitContainer.registeredKits.count, 2);

MParticleOptions *options = [MParticleOptions optionsWithKey:@"unit-test-key" secret:@"unit-test-secret"];
[[MParticle sharedInstance] startWithOptions:options];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
XCTAssertEqual(MPKitContainer.registeredKits.count, 2);
[[MParticle sharedInstance] switchWorkspaceWithOptions:options];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
XCTAssertEqual(MPKitContainer.registeredKits.count, 2);
[expectation fulfill];
});
});

[self waitForExpectationsWithTimeout:3 handler:nil];
}

// Kits with configurations that don't implement `stop` should be removed from the registry because they can't be cleanly restarted
- (void)testSwitchWorkspaceKitsWithoutStop {
XCTestExpectation *expectation = [self expectationWithDescription:@"async work"];

XCTAssertEqual(MPKitContainer.registeredKits.count, 0);
MPKitRegister *registerNoStop = [[MPKitRegister alloc] initWithName:@"TestKitNoStop" className:@"MPKitTestClassNoStartImmediately"];
[MParticle registerExtension:registerNoStop];

MParticleOptions *options = [MParticleOptions optionsWithKey:@"unit-test-key" secret:@"unit-test-secret"];
[[MParticle sharedInstance] startWithOptions:options];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
registerNoStop.wrapperInstance = [[MPKitTestClassNoStartImmediately alloc] init];
[MParticle sharedInstance].kitContainer.kitConfigurations[@42] = [[MPKitConfiguration alloc] init];

XCTAssertEqual(MPKitContainer.registeredKits.count, 1);

[[MParticle sharedInstance] switchWorkspaceWithOptions:options];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
XCTAssertEqual(MPKitContainer.registeredKits.count, 0);
[expectation fulfill];
});
});

[self waitForExpectationsWithTimeout:3 handler:nil];
}

// Kits with configurations that implement `stop` shouldn't be removed from the registry because they can be cleanly restarted
- (void)testSwitchWorkspaceKitsWithStop {
XCTestExpectation *expectation = [self expectationWithDescription:@"async work"];

XCTAssertEqual(MPKitContainer.registeredKits.count, 0);
MPKitRegister *registerWithStop = [[MPKitRegister alloc] initWithName:@"TestKitWithStop" className:@"MPKitTestClassNoStartImmediatelyWithStop"];
[MParticle registerExtension:registerWithStop];

MParticleOptions *options = [MParticleOptions optionsWithKey:@"unit-test-key" secret:@"unit-test-secret"];
[[MParticle sharedInstance] startWithOptions:options];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
registerWithStop.wrapperInstance = [[MPKitTestClassNoStartImmediatelyWithStop alloc] init];
[MParticle sharedInstance].kitContainer.kitConfigurations[@43] = [[MPKitConfiguration alloc] init];

XCTAssertEqual(MPKitContainer.registeredKits.count, 1);

[[MParticle sharedInstance] switchWorkspaceWithOptions:options];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
XCTAssertEqual(MPKitContainer.registeredKits.count, 1);
[expectation fulfill];
});
});

[self waitForExpectationsWithTimeout:3 handler:nil];
}

@end
2 changes: 1 addition & 1 deletion mParticle-Apple-SDK/Include/MPKitProtocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@

#pragma mark Kit lifecycle
- (void)start;
- (void)deinit;
- (void)stop;

#pragma mark Application
- (nonnull MPKitExecStatus *)continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(void(^ _Nonnull)(NSArray * _Nullable restorableObjects))restorationHandler;
Expand Down
36 changes: 34 additions & 2 deletions mParticle-Apple-SDK/Include/mParticle.h
Original file line number Diff line number Diff line change
Expand Up @@ -617,13 +617,35 @@ Defaults to false. Prevents the eventsHost above from overwriting the alias endp

/**
Starts the SDK with your API key and secret and installation type.
It is required that you use either this method or `start` to authorize the SDK before
It is required that you use either this method to authorize the SDK before
using the other API methods. The apiKey and secret that are passed in to this method
will override the api_key and api_secret parameters of the (optional) MParticleConfig.plist.
will override the `api_key` and `api_secret` parameters of the (optional) MParticleConfig.plist.
@param options SDK startup options
*/
- (void)startWithOptions:(MParticleOptions *)options;

/**
Switches the SDK to a new API key and secret.
Will first attempt to upload any batches that have not been sent to mParticle,
then all SDK state including user defaults, database, etc will be completely reset.
After that, `startWithOptions` will be called with the new key and secret
and the SDK will initialize again as if it is a new app launch.
Any kits that do not implement the `stop` method will be deactivated and will
not receive any events until the app is restarted. Any kits that were not used by the
previous workspace will continue to be available even if they don't implement `stop`.
Any sideloaded kits will need new instances passed in via `options.sideloadedKits`.
It is recommended to implement the `stop` in all sideloaded kits, though if it is not
implemented then the old instances will still not receive any new events.
The apiKey and secret that are passed in to this method will override the `api_key`
and `api_secret` parameters of the (optional) MParticleConfig.plist.
@param options SDK startup options
*/
- (void)switchWorkspaceWithOptions:(MParticleOptions *)options;

#pragma mark - Application notifications
#if TARGET_OS_IOS == 1

Expand Down Expand Up @@ -705,6 +727,16 @@ Defaults to false. Prevents the eventsHost above from overwriting the alias endp
*/
- (void)reset;

/**
This method will permanently remove ALL MParticle data from the device, including MParticle UserDefaults and Database, it will also halt any further upload or download behavior that may be prepared
If you have any reference to the MParticle instance, you must remove your reference by setting it to "nil", in order to avoid any unexpected behavior
The SDK will be shut down and [MParticle sharedInstance] will return a new instance without apiKey or secretKey. MParticle can be restarted by calling MParticle.startWithOptions
@param completion A block to execute on the main thread after the SDK is completely reset
*/
- (void)reset:(nullable void (^)(void))completion;

#pragma mark - Basic Tracking
/**
Contains a collection with all active timed events (timed events that had begun, but not yet ended). You should not keep a
Expand Down
Loading

0 comments on commit 3c3890d

Please sign in to comment.