Skip to content

Commit a9231ee

Browse files
authored
Delay Subscription Observer Notification (#327)
* Fix Extension Crash • Fixes an issue (#322) where exceptions that occurred while downloading attachments were not handled correctly. • This would have occurred in different situations. For example, if a device is out of space and doesn't have enough storage to download an attachment, it would throw an exception and crash the extension. • This commit would obviously not resolve the fact that the attachment cannot be downloaded for various reasons, but it will stop unnecessary crashes and crash reports being sent to developers. * Delay Subscription Settings Notification • Makes a change so that if the user accepts the Notification Permission prompt, the SDK will delay the OSSubscription observer update until after the HTTP request to update the OneSignal has finished. • This prevents a bug where, on some occasions when developers used the OSSubscription observer method to send out a push notification, the notification would fail because the observer method got called instantly the moment the user tapped 'Accept'. • This caused a race condition where the HTTP request to update the user's subscription status to Accepted, and the HTTP request to send a push notification to this user, were both sent at the same time, causing it to sometimes fail. * Added Test for Delayed Subscription Observer • Added a test for making sure that the subscription observer doesn't fire immediately • Changed OneSignalClientOverrider to let it switch threads before executing the HTTP callback so that it simulates the delay for a network request • Created a separate queue for OneSignalClientOverrider so that XCTests can call runThreads to finish pending HTTP request calls • This appears to have resolved some issues we've had with thread synchronization in our tests • Added some clang compiler flags to ignore warnings about undeclared selectors in places where it is unnecessary * Removed shouldDelaySubscriptionSettingsUpdate() from OneSignal.h • Method shouldDelaySubscriptionSettingsUpdate() should not be exposed to clients. * Minor Fixes • Removes an unnecessary property (subscriptionLock) and an unnecessary define (EXECUTION_LOCK) • Also removes an unnecessary comment
1 parent b46bd0f commit a9231ee

File tree

10 files changed

+598
-501
lines changed

10 files changed

+598
-501
lines changed

iOS_SDK/OneSignalSDK/Source/OSObservable.m

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,35 +50,47 @@ - (instancetype)init {
5050
}
5151

5252
- (void)addObserver:(id)observer {
53-
[observers addObject:observer];
53+
@synchronized(observers) {
54+
[observers addObject:observer];
55+
}
5456
}
5557

5658
- (void)removeObserver:(id)observer {
57-
[observers removeObject:observer];
59+
@synchronized(observers) {
60+
[observers removeObject:observer];
61+
}
5862
}
5963

6064
#pragma clang diagnostic push
6165
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
6266

6367
- (BOOL)notifyChange:(id)state {
6468
BOOL fired = false;
65-
for (id observer in observers) {
66-
fired = true;
67-
if (changeSelector) {
68-
// Any Obserable setup to fire a custom selector with changeSelector
69-
// is external to our SDK. Run on the main thread in case the
70-
// app developer needs to update UI elements.
71-
[OneSignalHelper dispatch_async_on_main_queue: ^{
72-
[observer performSelector:changeSelector withObject:state];
73-
}];
74-
}
75-
else
76-
[observer onChanged:state];
77-
}
69+
70+
@synchronized(observers) {
71+
for (id observer in observers) {
72+
fired = true;
73+
if (changeSelector) {
74+
// Any Obserable setup to fire a custom selector with changeSelector
75+
// is external to our SDK. Run on the main thread in case the
76+
// app developer needs to update UI elements.
77+
78+
[self callObserver:observer withSelector:changeSelector withState:state];
79+
80+
} else
81+
[observer onChanged:state];
82+
}
83+
}
7884

7985
return fired;
8086
}
8187

88+
- (void)callObserver:(id)observer withSelector:(SEL)selector withState:(id)state {
89+
[OneSignalHelper dispatch_async_on_main_queue:^{
90+
[observer performSelector:changeSelector withObject:state];
91+
}];
92+
}
93+
8294
#pragma clang diagnostic pop
8395

8496
@end

iOS_SDK/OneSignalSDK/Source/OSSubscription.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ typedef OSObservable<NSObject<OSSubscriptionStateObserver>*, OSSubscriptionState
6969
- (void)setAccepted:(BOOL)inAccpeted;
7070
- (void)persistAsFrom;
7171
- (BOOL)compare:(OSSubscriptionState*)from;
72+
73+
@property (nonatomic) BOOL delayedObserverUpdate;
74+
7275
@end
7376

7477
// Redefine OSSubscriptionStateChanges

iOS_SDK/OneSignalSDK/Source/OSSubscription.m

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
#import "OSSubscription.h"
2929

3030

31+
#pragma clang diagnostic push
32+
#pragma clang diagnostic ignored "-Wundeclared-selector"
33+
3134
@implementation OSSubscriptionState
3235

3336
- (ObserableSubscriptionStateType*)observable {
@@ -126,6 +129,16 @@ - (void)setUserSubscriptionSetting:(BOOL)userSubscriptionSetting {
126129

127130
- (void)setAccepted:(BOOL)inAccpeted {
128131
BOOL lastSubscribed = self.subscribed;
132+
133+
// checks to see if we should delay the observer update
134+
// This is to prevent a problem where the observer gets updated
135+
// before the OneSignal server does. (11f7f49841339317a334c5ec928db7edccb21cfe)
136+
137+
if ([OneSignal performSelector:@selector(shouldDelaySubscriptionSettingsUpdate)]) {
138+
self.delayedObserverUpdate = true;
139+
return;
140+
}
141+
129142
_accpeted = inAccpeted;
130143
if (lastSubscribed != self.subscribed)
131144
[self.observable notifyChange:self];

iOS_SDK/OneSignalSDK/Source/OneSignal.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,6 @@ typedef NS_ENUM(NSUInteger, ONE_S_LOG_LEVEL) {
341341
+ (void)deleteTags:(NSArray*)keys onSuccess:(OSResultSuccessBlock)successBlock onFailure:(OSFailureBlock)failureBlock;
342342
+ (void)deleteTags:(NSArray*)keys;
343343
+ (void)deleteTagsWithJsonString:(NSString*)jsonString;
344-
345344
// Optional method that sends us the user's email as an anonymized hash so that we can better target and personalize notifications sent to that user across their devices.
346345
// Sends as MD5 and SHA1 of the provided email
347346
+ (void)syncHashedEmail:(NSString*)email;

iOS_SDK/OneSignalSDK/Source/OneSignal.m

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ @implementation OneSignal
126126
static NSString* mSDKType = @"native";
127127
static BOOL coldStartFromTapOnNotification = NO;
128128

129+
static BOOL shouldDelaySubscriptionUpdate = false;
130+
129131
static NSMutableArray* pendingSendTagCallbacks;
130132
static OSResultSuccessBlock pendingGetTagsSuccessBlock;
131133
static OSFailureBlock pendingGetTagsFailureBlock;
@@ -294,7 +296,6 @@ + (void)clearStatics {
294296
_currentSubscriptionState = nil;
295297

296298
_permissionStateChangesObserver = nil;
297-
_subscriptionStateChangesObserver = nil;
298299
}
299300

300301
// Set to false as soon as it's read.
@@ -303,6 +304,10 @@ + (BOOL)coldStartFromTapOnNotification {
303304
coldStartFromTapOnNotification = NO;
304305
return val;
305306
}
307+
308+
+ (BOOL)shouldDelaySubscriptionSettingsUpdate {
309+
return shouldDelaySubscriptionUpdate;
310+
}
306311

307312
+ (id)initWithLaunchOptions:(NSDictionary*)launchOptions appId:(NSString*)appId {
308313
return [self initWithLaunchOptions: launchOptions appId: appId handleNotificationReceived: NULL handleNotificationAction : NULL settings: @{kOSSettingsKeyAutoPrompt : @YES, kOSSettingsKeyInAppAlerts : @YES, kOSSettingsKeyInAppLaunchURL : @YES}];
@@ -817,6 +822,8 @@ + (void)setSubscription:(BOOL)enable {
817822
[[NSUserDefaults standardUserDefaults] setObject:value forKey:@"ONESIGNAL_SUBSCRIPTION"];
818823
[[NSUserDefaults standardUserDefaults] synchronize];
819824

825+
shouldDelaySubscriptionUpdate = true;
826+
820827
self.currentSubscriptionState.userSubscriptionSetting = enable;
821828

822829
if (app_id)
@@ -1113,7 +1120,17 @@ + (BOOL) sendNotificationTypesUpdate {
11131120

11141121
mLastNotificationTypes = [self getNotificationTypes];
11151122

1116-
[OneSignalClient.sharedClient executeRequest:[OSRequestUpdateNotificationTypes withUserId:self.currentSubscriptionState.userId appId:self.app_id notificationTypes:@([self getNotificationTypes])] onSuccess:nil onFailure:nil];
1123+
//delays observer update until the OneSignal server is notified
1124+
shouldDelaySubscriptionUpdate = true;
1125+
1126+
[OneSignalClient.sharedClient executeRequest:[OSRequestUpdateNotificationTypes withUserId:self.currentSubscriptionState.userId appId:self.app_id notificationTypes:@([self getNotificationTypes])] onSuccess:^(NSDictionary *result) {
1127+
1128+
shouldDelaySubscriptionUpdate = false;
1129+
1130+
if (self.currentSubscriptionState.delayedObserverUpdate)
1131+
[self.currentSubscriptionState setAccepted:[self getNotificationTypes] > 0];
1132+
1133+
} onFailure:nil];
11171134

11181135
if ([self getUsableDeviceToken])
11191136
[self fireIdsAvailableCallback];

iOS_SDK/OneSignalSDK/Source/OneSignalHelper.m

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -725,20 +725,29 @@ + (NSString*)downloadMediaAndSaveInBundle:(NSString*)url {
725725
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
726726
NSString* filePath = [paths[0] stringByAppendingPathComponent:name];
727727

728-
NSError* error = nil;
729-
[NSURLSession downloadItemAtURL:URL toFile:filePath error:error];
730-
NSArray* cachedFiles = [[NSUserDefaults standardUserDefaults] objectForKey:@"CACHED_MEDIA"];
731-
NSMutableArray* appendedCache;
732-
if (cachedFiles) {
733-
appendedCache = [[NSMutableArray alloc] initWithArray:cachedFiles];
734-
[appendedCache addObject:name];
728+
//guard against situations where for example, available storage is too low
729+
730+
@try {
731+
NSError* error = nil;
732+
[NSURLSession downloadItemAtURL:URL toFile:filePath error:error];
733+
NSArray* cachedFiles = [[NSUserDefaults standardUserDefaults] objectForKey:@"CACHED_MEDIA"];
734+
NSMutableArray* appendedCache;
735+
if (cachedFiles) {
736+
appendedCache = [[NSMutableArray alloc] initWithArray:cachedFiles];
737+
[appendedCache addObject:name];
738+
}
739+
else
740+
appendedCache = [[NSMutableArray alloc] initWithObjects:name, nil];
741+
742+
[[NSUserDefaults standardUserDefaults] setObject:appendedCache forKey:@"CACHED_MEDIA"];
743+
[[NSUserDefaults standardUserDefaults] synchronize];
744+
return name;
745+
} @catch (NSException *exception) {
746+
[OneSignal onesignal_Log:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"OneSignal encountered an exception while downloading file (%@), exception: %@", url, exception.description]];
747+
748+
return nil;
735749
}
736-
else
737-
appendedCache = [[NSMutableArray alloc] initWithObjects:name, nil];
738-
739-
[[NSUserDefaults standardUserDefaults] setObject:appendedCache forKey:@"CACHED_MEDIA"];
740-
[[NSUserDefaults standardUserDefaults] synchronize];
741-
return name;
750+
742751
}
743752

744753
+(void)clearCachedMedia {

iOS_SDK/OneSignalSDK/UnitTests/Shadows/OneSignalClientOverrider.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
+(int)networkRequestCount;
1818
+(void)setLastUrl:(NSString*)value;
1919
+(NSString*)lastUrl;
20-
20+
+(void)setShouldExecuteInstantaneously:(BOOL)instant;
21+
+ (dispatch_queue_t)getHTTPQueue;
2122
@end
2223

iOS_SDK/OneSignalSDK/UnitTests/Shadows/OneSignalClientOverrider.m

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,43 @@
2323
#import "OneSignalRequest.h"
2424
#import "OneSignalSelectorHelpers.h"
2525

26+
#pragma GCC diagnostic push
27+
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
28+
2629
@implementation OneSignalClientOverrider
2730

2831
static dispatch_queue_t serialMockMainLooper;
2932
static NSString* lastUrl;
3033
static int networkRequestCount;
3134
static NSDictionary* lastHTTPRequest;
3235
static XCTestCase* currentTestInstance;
36+
static BOOL executeInstantaneously = true;
37+
static dispatch_queue_t executionQueue;
3338

3439
+ (void)load {
3540
serialMockMainLooper = dispatch_queue_create("com.onesignal.unittest", DISPATCH_QUEUE_SERIAL);
3641

3742

3843
//with refactored networking code, need to replace the implementation of the execute request method so tests don't actually execite HTTP requests
3944
injectToProperClass(@selector(overrideExecuteRequest:onSuccess:onFailure:), @selector(executeRequest:onSuccess:onFailure:), @[], [OneSignalClientOverrider class], [OneSignalClient class]);
45+
46+
executionQueue = dispatch_queue_create("com.onesignal.execution", NULL);
4047
}
4148

4249
- (void)overrideExecuteRequest:(OneSignalRequest *)request onSuccess:(OSResultSuccessBlock)successBlock onFailure:(OSFailureBlock)failureBlock {
50+
NSLog(@"Executing request: %@", NSStringFromClass([request class]));
51+
if (executeInstantaneously) {
52+
[OneSignalClientOverrider finishExecutingRequest:request onSuccess:successBlock onFailure:failureBlock];
53+
} else {
54+
dispatch_async(executionQueue, ^{
55+
[OneSignalClientOverrider finishExecutingRequest:request onSuccess:successBlock onFailure:failureBlock];
56+
});
57+
}
58+
}
59+
60+
+ (void)finishExecutingRequest:(OneSignalRequest *)request onSuccess:(OSResultSuccessBlock)successBlock onFailure:(OSFailureBlock)failureBlock {
61+
NSLog(@"completing HTTP request: %@", NSStringFromClass([request class]));
62+
4363
NSMutableDictionary *parameters = [request.parameters mutableCopy];
4464

4565
if (!parameters[@"app_id"] && ![request.request.URL.absoluteString containsString:@"/apps/"])
@@ -62,6 +82,14 @@ - (void)overrideExecuteRequest:(OneSignalRequest *)request onSuccess:(OSResultSu
6282
}
6383
}
6484

85+
+(dispatch_queue_t)getHTTPQueue {
86+
return executionQueue;
87+
}
88+
89+
+(void)setShouldExecuteInstantaneously:(BOOL)instant {
90+
executeInstantaneously = instant;
91+
}
92+
6593
+(void)reset:(XCTestCase*)testInstance {
6694
currentTestInstance = testInstance;
6795

iOS_SDK/OneSignalSDK/UnitTests/Shadows/OneSignalHelperOverrider.m

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
#import "OneSignal.h"
3333
#import "OneSignalHelper.h"
3434

35+
#pragma clang diagnostic push
36+
#pragma clang diagnostic ignored "-Wundeclared-selector"
37+
3538
@implementation OneSignalHelperOverrider
3639

3740
static dispatch_queue_t serialMockMainLooper;

0 commit comments

Comments
 (0)