diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index 6442b63b2..938c4914c 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -463,10 +463,23 @@ static BOOL ComputeInitialStorageReadable(void) { if (hasPriorSession) { // returning user during prewarm; defer return NO; } - if ([NSThread isMainThread]) { - return UIApplication.sharedApplication.isProtectedDataAvailable; + if (![NSThread isMainThread]) { + return YES; // off-main: can't safely read UIApplication } - return YES; // off-main: can't safely read UIApplication + UIApplication *sharedApp = UIApplication.sharedApplication; + if (sharedApp) { + return sharedApp.isProtectedDataAvailable; + } + // sharedApplication is nil: we are running before UIApplicationMain, e.g. a SwiftUI + // App.init(). + // A user-initiated launch implies the device is unlocked, so storage is readable. + // Only a prewarm-created process can reach this point while storage may still be + // locked; iOS marks those processes with the ActivePrewarm environment variable, + // which is cleared after launch completes, but pre-UIApplicationMain is always + // before that, so the read is reliable here. + BOOL isPrewarm = [NSProcessInfo.processInfo.environment[@"ActivePrewarm"] isEqualToString:@"1"]; + [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:[NSString stringWithFormat:@"OneSignal initialized before UIApplicationMain (ActivePrewarm=%d); %@", isPrewarm, isPrewarm ? @"deferring storage reads until launch completes" : @"treating storage as readable"]]; + return !isPrewarm; } /// One-time setup of the protected-data readiness check that matters during iOS app @@ -474,43 +487,74 @@ static BOOL ComputeInitialStorageReadable(void) { /// /// The cached flag is a one-way latch (NO → YES, never reverses). It is: /// * Seeded by `ComputeInitialStorageReadable` (case table below). -/// * Flipped to YES on `UIApplicationProtectedDataDidBecomeAvailable`. +/// * Flipped to YES on `UIApplicationProtectedDataDidBecomeAvailable`, or on +/// `didFinishLaunching`/`didBecomeActive` when `isProtectedDataAvailable` verifies YES. /// * Read via `OneSignalConfig.isProtectedDataAvailableProvider` /// /// Seed case table: -/// 1. pushModels populated → YES (UD is readable) -/// 2. `keyHasPriorSession` set, no UD → NO (returning user during prewarm; defer) -/// 3. neither + main thread → fall back to `UIApplication.isProtectedDataAvailable` -/// 4. neither + off-main thread → YES (can't safely read UIApplication) +/// 1. pushModels populated → YES (UD is readable) +/// 2. `keyHasPriorSession` set, no UD → NO (returning user during prewarm; defer) +/// 3. neither + off-main thread → YES (can't safely read UIApplication) +/// 4. neither + sharedApplication exists → `UIApplication.isProtectedDataAvailable` +/// 5. neither + sharedApplication nil → NO only when `ActivePrewarm=1` (pre-UIApplicationMain: +/// a user-initiated launch implies unlocked storage; only prewarm can mean locked storage) /// -/// `keyHasPriorSession` is the only reliable "SDK previously ran here" sentinel, and case 3's +/// `keyHasPriorSession` is the only reliable "SDK previously ran here" sentinel, and case 4's /// `isProtectedDataAvailable` tiebreaker also protects SDK upgraders who never wrote `keyHasPriorSession`. /// -/// `gObserverShouldRecover` is set when init defers (storage isn't yet readable) and cleared by the -/// observer's first fire (tracked since `DidBecomeAvailable` posts on every device unlock). +/// `gObserverShouldRecover` is set when init defers (storage isn't yet readable) and cleared when +/// recovery runs once. Recovery triggers, whichever verifies first: +/// * `UIApplicationProtectedDataDidBecomeAvailable` posts on unlock, including the +/// first unlock after boot (the locked-prewarm case). Does NOT post if storage was +/// never locked, so it cannot be the only trigger. +/// * `didFinishLaunching` / `didBecomeActive` with a live `isProtectedDataAvailable` == YES +/// re-check covers deferrals where storage was readable all along (e.g. a prewarm-created +/// process the user later foregrounds, when the unlock notification never fires). + (void)setupProtectedDataObserverOnce { static _Atomic(BOOL) gProtectedDataAvailable = NO; static _Atomic(BOOL) gObserverShouldRecover = NO; static dispatch_once_t protectedDataOnce; dispatch_once(&protectedDataOnce, ^{ - [NSNotificationCenter.defaultCenter addObserverForName:UIApplicationProtectedDataDidBecomeAvailable - object:nil - queue:nil - usingBlock:^(NSNotification * _Nonnull note) { + // Marks storage readable, then re-drives the SDK components that init skipped + // (at most once). If `gObserverShouldRecover == YES`, atomically swap to NO and + // proceed; otherwise bail (already consumed, or init never deferred). + void (^recoverIfDeferred)(void) = ^{ atomic_store(&gProtectedDataAvailable, YES); - // Only run the recovery if init deferred. If `gObserverShouldRecover == YES`, - // atomically swap to NO and proceed; otherwise bail (already consumed, or never set). BOOL shouldRecover = YES; if (!atomic_compare_exchange_strong(&gObserverShouldRecover, &shouldRecover, NO)) { return; } + [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:@"Device storage became readable; starting deferred OneSignal components"]; [OneSignalUserManagerImpl.sharedInstance start]; [OSNotificationsManager sendPushTokenToDelegate]; [OneSignal startLiveActivitiesManager]; [OneSignal startInAppMessages]; [OneSignal startNewSession:YES]; + }; + + // Authoritative signal: the OS says protected data just became available. + [NSNotificationCenter.defaultCenter addObserverForName:UIApplicationProtectedDataDidBecomeAvailable + object:nil + queue:nil + usingBlock:^(NSNotification * _Nonnull note) { + recoverIfDeferred(); }]; + // Lifecycle signals: if init deferred while storage was actually readable, + // the unlock notification never posts. Once UIApplicationMain has run, + // `sharedApplication` exists and we can consult the real `isProtectedDataAvailable`. + for (NSNotificationName name in @[UIApplicationDidFinishLaunchingNotification, + UIApplicationDidBecomeActiveNotification]) { + [NSNotificationCenter.defaultCenter addObserverForName:name + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification * _Nonnull note) { + if (UIApplication.sharedApplication.isProtectedDataAvailable) { + recoverIfDeferred(); + } + }]; + } + OneSignalConfig.isProtectedDataAvailableProvider = ^BOOL { return atomic_load(&gProtectedDataAvailable); };