mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			517 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Objective-C
		
	
			
		
		
	
	
			517 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Objective-C
		
	
| //
 | |
| //  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
 | |
| //
 | |
| 
 | |
| #import "TSAccountManager.h"
 | |
| #import "AppContext.h"
 | |
| #import "AppReadiness.h"
 | |
| #import "NSNotificationCenter+OWS.h"
 | |
| #import "ProfileManagerProtocol.h"
 | |
| #import "SSKEnvironment.h"
 | |
| #import "YapDatabaseConnection+OWS.h"
 | |
| #import "YapDatabaseTransaction+OWS.h"
 | |
| #import <PromiseKit/AnyPromise.h>
 | |
| #import <Reachability/Reachability.h>
 | |
| #import <SignalCoreKit/NSData+OWS.h>
 | |
| #import <SignalCoreKit/Randomness.h>
 | |
| #import <SessionMessagingKit/SessionMessagingKit-Swift.h>
 | |
| #import <YapDatabase/YapDatabase.h>
 | |
| 
 | |
| NS_ASSUME_NONNULL_BEGIN
 | |
| 
 | |
| NSString *const TSRegistrationErrorDomain = @"TSRegistrationErrorDomain";
 | |
| NSString *const TSRegistrationErrorUserInfoHTTPStatus = @"TSHTTPStatus";
 | |
| NSString *const RegistrationStateDidChangeNotification = @"RegistrationStateDidChangeNotification";
 | |
| NSString *const kNSNotificationName_LocalNumberDidChange = @"kNSNotificationName_LocalNumberDidChange";
 | |
| 
 | |
| NSString *const TSAccountManager_RegisteredNumberKey = @"TSStorageRegisteredNumberKey";
 | |
| NSString *const TSAccountManager_IsDeregisteredKey = @"TSAccountManager_IsDeregisteredKey";
 | |
| NSString *const TSAccountManager_ReregisteringPhoneNumberKey = @"TSAccountManager_ReregisteringPhoneNumberKey";
 | |
| NSString *const TSAccountManager_LocalRegistrationIdKey = @"TSStorageLocalRegistrationId";
 | |
| NSString *const TSAccountManager_HasPendingRestoreDecisionKey = @"TSAccountManager_HasPendingRestoreDecisionKey";
 | |
| 
 | |
| NSString *const TSAccountManager_UserAccountCollection = @"TSStorageUserAccountCollection";
 | |
| NSString *const TSAccountManager_ServerAuthToken = @"TSStorageServerAuthToken";
 | |
| NSString *const TSAccountManager_ServerSignalingKey = @"TSStorageServerSignalingKey";
 | |
| NSString *const TSAccountManager_ManualMessageFetchKey = @"TSAccountManager_ManualMessageFetchKey";
 | |
| NSString *const TSAccountManager_NeedsAccountAttributesUpdateKey = @"TSAccountManager_NeedsAccountAttributesUpdateKey";
 | |
| 
 | |
| @interface TSAccountManager ()
 | |
| 
 | |
| @property (atomic, readonly) BOOL isRegistered;
 | |
| 
 | |
| @property (nonatomic, nullable) NSString *cachedLocalNumber;
 | |
| @property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
 | |
| 
 | |
| @property (nonatomic, nullable) NSNumber *cachedIsDeregistered;
 | |
| 
 | |
| @property (nonatomic) Reachability *reachability;
 | |
| 
 | |
| @end
 | |
| 
 | |
| #pragma mark -
 | |
| 
 | |
| @implementation TSAccountManager
 | |
| 
 | |
| @synthesize isRegistered = _isRegistered;
 | |
| 
 | |
| - (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage
 | |
| {
 | |
|     self = [super init];
 | |
|     if (!self) {
 | |
|         return self;
 | |
|     }
 | |
| 
 | |
|     _dbConnection = [primaryStorage newDatabaseConnection];
 | |
|     self.reachability = [Reachability reachabilityForInternetConnection];
 | |
| 
 | |
|     if (!CurrentAppContext().isMainApp) {
 | |
|         [[NSNotificationCenter defaultCenter] addObserver:self
 | |
|                                                  selector:@selector(yapDatabaseModifiedExternally:)
 | |
|                                                      name:YapDatabaseModifiedExternallyNotification
 | |
|                                                    object:nil];
 | |
|     }
 | |
| 
 | |
|     [AppReadiness runNowOrWhenAppDidBecomeReady:^{
 | |
|         [[self updateAccountAttributesIfNecessary] retainUntilComplete];
 | |
|     }];
 | |
| 
 | |
|     [[NSNotificationCenter defaultCenter] addObserver:self
 | |
|                                              selector:@selector(reachabilityChanged)
 | |
|                                                  name:kReachabilityChangedNotification
 | |
|                                                object:nil];
 | |
| 
 | |
|     return self;
 | |
| }
 | |
| 
 | |
| - (void)dealloc
 | |
| {
 | |
|     [[NSNotificationCenter defaultCenter] removeObserver:self];
 | |
| }
 | |
| 
 | |
| + (instancetype)sharedInstance
 | |
| {
 | |
|     return SSKEnvironment.shared.tsAccountManager;
 | |
| }
 | |
| 
 | |
| #pragma mark - Dependencies
 | |
| 
 | |
| - (id<ProfileManagerProtocol>)profileManager {
 | |
|     return SSKEnvironment.shared.profileManager;
 | |
| }
 | |
| 
 | |
| #pragma mark -
 | |
| 
 | |
| - (void)setPhoneNumberAwaitingVerification:(NSString *_Nullable)phoneNumberAwaitingVerification
 | |
| {
 | |
|     _phoneNumberAwaitingVerification = phoneNumberAwaitingVerification;
 | |
| 
 | |
|     [[NSNotificationCenter defaultCenter] postNotificationNameAsync:kNSNotificationName_LocalNumberDidChange
 | |
|                                                              object:nil
 | |
|                                                            userInfo:nil];
 | |
| }
 | |
| 
 | |
| - (OWSRegistrationState)registrationState
 | |
| {
 | |
|     if (!self.isRegistered) {
 | |
|         return OWSRegistrationState_Unregistered;
 | |
|     } else if (self.isDeregistered) {
 | |
|         if (self.isReregistering) {
 | |
|             return OWSRegistrationState_Reregistering;
 | |
|         } else {
 | |
|             return OWSRegistrationState_Deregistered;
 | |
|         }
 | |
|     } else if (self.isDeregistered) {
 | |
|         return OWSRegistrationState_PendingBackupRestore;
 | |
|     } else {
 | |
|         return OWSRegistrationState_Registered;
 | |
|     }
 | |
| }
 | |
| 
 | |
| - (BOOL)isRegistered
 | |
| {
 | |
|     @synchronized (self) {
 | |
|         if (_isRegistered) {
 | |
|             return YES;
 | |
|         } else {
 | |
|             // Cache this once it's true since it's called alot, involves a dbLookup, and once set - it doesn't change.
 | |
|             _isRegistered = [self storedLocalNumber] != nil;
 | |
|         }
 | |
|         return _isRegistered;
 | |
|     }
 | |
| }
 | |
| 
 | |
| - (BOOL)isRegisteredAndReady
 | |
| {
 | |
|     return self.registrationState == OWSRegistrationState_Registered;
 | |
| }
 | |
| 
 | |
| - (void)didRegister
 | |
| {
 | |
|     NSString *phoneNumber = self.phoneNumberAwaitingVerification;
 | |
| 
 | |
|     [self storeLocalNumber:phoneNumber];
 | |
| 
 | |
|     // Warm these cached values.
 | |
|     [self isRegistered];
 | |
|     [self localNumber];
 | |
|     [self isDeregistered];
 | |
| 
 | |
|     [self postRegistrationStateDidChangeNotification];
 | |
| }
 | |
| 
 | |
| + (nullable NSString *)localNumber
 | |
| {
 | |
|     return [[self sharedInstance] localNumber];
 | |
| }
 | |
| 
 | |
| - (nullable NSString *)localNumber
 | |
| {
 | |
|     NSString *awaitingVerif = self.phoneNumberAwaitingVerification;
 | |
|     if (awaitingVerif) {
 | |
|         return awaitingVerif;
 | |
|     }
 | |
| 
 | |
|     // Cache this since we access this a lot, and once set it will not change.
 | |
|     @synchronized(self)
 | |
|     {
 | |
|         if (self.cachedLocalNumber == nil) {
 | |
|             self.cachedLocalNumber = self.storedLocalNumber;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return self.cachedLocalNumber;
 | |
| }
 | |
| 
 | |
| - (nullable NSString *)storedLocalNumber
 | |
| {
 | |
|     @synchronized (self) {
 | |
|         return [self.dbConnection stringForKey:TSAccountManager_RegisteredNumberKey
 | |
|                                   inCollection:TSAccountManager_UserAccountCollection];
 | |
|     }
 | |
| }
 | |
| 
 | |
| - (nullable NSString *)storedOrCachedLocalNumber:(YapDatabaseReadTransaction *)transaction
 | |
| {
 | |
|     @synchronized(self) {
 | |
|         if (self.cachedLocalNumber) {
 | |
|             return self.cachedLocalNumber;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return [transaction stringForKey:TSAccountManager_RegisteredNumberKey
 | |
|                         inCollection:TSAccountManager_UserAccountCollection];
 | |
| }
 | |
| 
 | |
| - (void)storeLocalNumber:(NSString *)localNumber
 | |
| {
 | |
|     @synchronized (self) {
 | |
|         [self.dbConnection setObject:localNumber
 | |
|                               forKey:TSAccountManager_RegisteredNumberKey
 | |
|                         inCollection:TSAccountManager_UserAccountCollection];
 | |
| 
 | |
|         [self.dbConnection removeObjectForKey:TSAccountManager_ReregisteringPhoneNumberKey
 | |
|                                  inCollection:TSAccountManager_UserAccountCollection];
 | |
| 
 | |
|         self.phoneNumberAwaitingVerification = nil;
 | |
| 
 | |
|         self.cachedLocalNumber = localNumber;
 | |
|     }
 | |
| }
 | |
| 
 | |
| + (uint32_t)getOrGenerateRegistrationId:(YapDatabaseReadWriteTransaction *)transaction
 | |
| {
 | |
|     return [[self sharedInstance] getOrGenerateRegistrationId:transaction];
 | |
| }
 | |
| 
 | |
| - (uint32_t)getOrGenerateRegistrationId
 | |
| {
 | |
|     __block uint32_t result;
 | |
|     [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
 | |
|         result = [self getOrGenerateRegistrationId:transaction];
 | |
|     }];
 | |
|     return result;
 | |
| }
 | |
| 
 | |
| - (uint32_t)getOrGenerateRegistrationId:(YapDatabaseReadWriteTransaction *)transaction
 | |
| {
 | |
|     // Unlike other methods in this class, there's no need for a `@synchronized` block
 | |
|     // here, since we're already in a write transaction, and all writes occur on a serial queue.
 | |
|     //
 | |
|     // Since other code in this class which uses @synchronized(self) also needs to open write
 | |
|     // transaction, using @synchronized(self) here, inside of a WriteTransaction risks deadlock.
 | |
|     uint32_t registrationID = [[transaction objectForKey:TSAccountManager_LocalRegistrationIdKey
 | |
|                                             inCollection:TSAccountManager_UserAccountCollection] unsignedIntValue];
 | |
| 
 | |
|     if (registrationID == 0) {
 | |
|         registrationID = (uint32_t)arc4random_uniform(16380) + 1;
 | |
| 
 | |
|         [transaction setObject:[NSNumber numberWithUnsignedInteger:registrationID]
 | |
|                         forKey:TSAccountManager_LocalRegistrationIdKey
 | |
|                   inCollection:TSAccountManager_UserAccountCollection];
 | |
|     }
 | |
|     return registrationID;
 | |
| }
 | |
| 
 | |
| - (void)registerForPushNotificationsWithPushToken:(NSString *)pushToken
 | |
|                                         voipToken:(NSString *)voipToken
 | |
|                                    isForcedUpdate:(BOOL)isForcedUpdate
 | |
|                                           success:(void (^)(void))successHandler
 | |
|                                           failure:(void (^)(NSError *))failureHandler
 | |
| {
 | |
|     [self registerForPushNotificationsWithPushToken:pushToken
 | |
|                                           voipToken:voipToken
 | |
|                                      isForcedUpdate:isForcedUpdate
 | |
|                                             success:successHandler
 | |
|                                             failure:failureHandler
 | |
|                                    remainingRetries:3];
 | |
| }
 | |
| 
 | |
| - (void)registerForPushNotificationsWithPushToken:(NSString *)pushToken
 | |
|                                         voipToken:(NSString *)voipToken
 | |
|                                    isForcedUpdate:(BOOL)isForcedUpdate
 | |
|                                           success:(void (^)(void))successHandler
 | |
|                                           failure:(void (^)(NSError *))failureHandler
 | |
|                                  remainingRetries:(int)remainingRetries
 | |
| {
 | |
|     BOOL isUsingFullAPNs = [NSUserDefaults.standardUserDefaults boolForKey:@"isUsingFullAPNs"];
 | |
|     NSData *pushTokenAsData = [NSData dataFromHexString:pushToken];
 | |
|     AnyPromise *promise = isUsingFullAPNs ? [LKPushNotificationAPI registerWithToken:pushTokenAsData hexEncodedPublicKey:self.localNumber isForcedUpdate:isForcedUpdate]
 | |
|         : [LKPushNotificationAPI unregisterToken:pushTokenAsData];
 | |
|     promise
 | |
|     .then(^() {
 | |
|         successHandler();
 | |
|     })
 | |
|     .catch(^(NSError *error) {
 | |
|         if (remainingRetries > 0) {
 | |
|             [self registerForPushNotificationsWithPushToken:pushToken voipToken:voipToken isForcedUpdate:isForcedUpdate success:successHandler failure:failureHandler
 | |
|                 remainingRetries:remainingRetries - 1];
 | |
|         } else {
 | |
|             failureHandler(error);
 | |
|         }
 | |
|     });
 | |
| }
 | |
| 
 | |
| - (void)rerequestSMSWithCaptchaToken:(nullable NSString *)captchaToken
 | |
|                              success:(void (^)(void))successBlock
 | |
|                              failure:(void (^)(NSError *error))failureBlock
 | |
| {
 | |
|     // TODO: Can we remove phoneNumberAwaitingVerification?
 | |
|     NSString *number          = self.phoneNumberAwaitingVerification;
 | |
| 
 | |
|     [self registerWithPhoneNumber:number
 | |
|                      captchaToken:captchaToken
 | |
|                           success:successBlock
 | |
|                           failure:failureBlock
 | |
|                   smsVerification:YES];
 | |
| }
 | |
| 
 | |
| - (void)rerequestVoiceWithCaptchaToken:(nullable NSString *)captchaToken
 | |
|                                success:(void (^)(void))successBlock
 | |
|                                failure:(void (^)(NSError *error))failureBlock
 | |
| {
 | |
|     NSString *number          = self.phoneNumberAwaitingVerification;
 | |
| 
 | |
|     [self registerWithPhoneNumber:number
 | |
|                      captchaToken:captchaToken
 | |
|                           success:successBlock
 | |
|                           failure:failureBlock
 | |
|                   smsVerification:NO];
 | |
| }
 | |
| 
 | |
| #pragma mark Server keying material
 | |
| 
 | |
| + (NSString *)generateNewAccountAuthenticationToken {
 | |
|     NSData *authToken = [Randomness generateRandomBytes:16];
 | |
|     NSString *authTokenPrint = [[NSData dataWithData:authToken] hexadecimalString];
 | |
|     return authTokenPrint;
 | |
| }
 | |
| 
 | |
| + (nullable NSString *)signalingKey
 | |
| {
 | |
|     return [[self sharedInstance] signalingKey];
 | |
| }
 | |
| 
 | |
| - (nullable NSString *)signalingKey
 | |
| {
 | |
|     return [self.dbConnection stringForKey:TSAccountManager_ServerSignalingKey
 | |
|                               inCollection:TSAccountManager_UserAccountCollection];
 | |
| }
 | |
| 
 | |
| + (nullable NSString *)serverAuthToken
 | |
| {
 | |
|     return [[self sharedInstance] serverAuthToken];
 | |
| }
 | |
| 
 | |
| - (nullable NSString *)serverAuthToken
 | |
| {
 | |
|     return [self.dbConnection stringForKey:TSAccountManager_ServerAuthToken
 | |
|                               inCollection:TSAccountManager_UserAccountCollection];
 | |
| }
 | |
| 
 | |
| - (void)storeServerAuthToken:(NSString *)authToken
 | |
| {
 | |
|     [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
 | |
|         [transaction setObject:authToken
 | |
|                         forKey:TSAccountManager_ServerAuthToken
 | |
|                   inCollection:TSAccountManager_UserAccountCollection];
 | |
|     }];
 | |
| }
 | |
| 
 | |
| - (void)yapDatabaseModifiedExternally:(NSNotification *)notification
 | |
| {
 | |
|     // Any database write by the main app might reflect a deregistration,
 | |
|     // so clear the cached "is registered" state.  This will significantly
 | |
|     // erode the value of this cache in the SAE.
 | |
|     @synchronized(self)
 | |
|     {
 | |
|         _isRegistered = NO;
 | |
|     }
 | |
| }
 | |
| 
 | |
| #pragma mark - De-Registration
 | |
| 
 | |
| - (BOOL)isDeregistered
 | |
| {
 | |
|     // Cache this since we access this a lot, and once set it will not change.
 | |
|     @synchronized(self) {
 | |
|         if (self.cachedIsDeregistered == nil) {
 | |
|             self.cachedIsDeregistered = @([self.dbConnection boolForKey:TSAccountManager_IsDeregisteredKey
 | |
|                                                            inCollection:TSAccountManager_UserAccountCollection
 | |
|                                                            defaultValue:NO]);
 | |
|         }
 | |
| 
 | |
|         return self.cachedIsDeregistered.boolValue;
 | |
|     }
 | |
| }
 | |
| 
 | |
| - (void)setIsDeregistered:(BOOL)isDeregistered
 | |
| {
 | |
|     @synchronized(self) {
 | |
|         if (self.cachedIsDeregistered && self.cachedIsDeregistered.boolValue == isDeregistered) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         self.cachedIsDeregistered = @(isDeregistered);
 | |
|     }
 | |
| 
 | |
|     [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
 | |
|         [transaction setObject:@(isDeregistered)
 | |
|                         forKey:TSAccountManager_IsDeregisteredKey
 | |
|                   inCollection:TSAccountManager_UserAccountCollection];
 | |
|     }];
 | |
| 
 | |
|     [self postRegistrationStateDidChangeNotification];
 | |
| }
 | |
| 
 | |
| - (nullable NSString *)reregisterationPhoneNumber
 | |
| {
 | |
|     NSString *_Nullable result = [self.dbConnection stringForKey:TSAccountManager_ReregisteringPhoneNumberKey
 | |
|                                                     inCollection:TSAccountManager_UserAccountCollection];
 | |
|     return result;
 | |
| }
 | |
| 
 | |
| - (BOOL)isReregistering
 | |
| {
 | |
|     return nil !=
 | |
|         [self.dbConnection stringForKey:TSAccountManager_ReregisteringPhoneNumberKey
 | |
|                            inCollection:TSAccountManager_UserAccountCollection];
 | |
| }
 | |
| 
 | |
| - (BOOL)hasPendingBackupRestoreDecision
 | |
| {
 | |
|     return [self.dbConnection boolForKey:TSAccountManager_HasPendingRestoreDecisionKey
 | |
|                             inCollection:TSAccountManager_UserAccountCollection
 | |
|                             defaultValue:NO];
 | |
| }
 | |
| 
 | |
| - (void)setHasPendingBackupRestoreDecision:(BOOL)value
 | |
| {
 | |
|     [self.dbConnection setBool:value
 | |
|                         forKey:TSAccountManager_HasPendingRestoreDecisionKey
 | |
|                   inCollection:TSAccountManager_UserAccountCollection];
 | |
| 
 | |
|     [self postRegistrationStateDidChangeNotification];
 | |
| }
 | |
| 
 | |
| - (BOOL)isManualMessageFetchEnabled
 | |
| {
 | |
|     return [self.dbConnection boolForKey:TSAccountManager_ManualMessageFetchKey
 | |
|                             inCollection:TSAccountManager_UserAccountCollection
 | |
|                             defaultValue:NO];
 | |
| }
 | |
| 
 | |
| - (AnyPromise *)setIsManualMessageFetchEnabled:(BOOL)value {
 | |
|     [self.dbConnection setBool:value
 | |
|                         forKey:TSAccountManager_ManualMessageFetchKey
 | |
|                   inCollection:TSAccountManager_UserAccountCollection];
 | |
| 
 | |
|     // Try to update the account attributes to reflect this change.
 | |
|     return [self updateAccountAttributes];
 | |
| }
 | |
| 
 | |
| - (void)registerForTestsWithLocalNumber:(NSString *)localNumber
 | |
| {    
 | |
|     [self storeLocalNumber:localNumber];
 | |
| }
 | |
| 
 | |
| #pragma mark - Account Attributes
 | |
| 
 | |
| - (AnyPromise *)updateAccountAttributes {
 | |
|     // Enqueue a "account attribute update", recording the "request time".
 | |
|     [self.dbConnection setObject:[NSDate new]
 | |
|                           forKey:TSAccountManager_NeedsAccountAttributesUpdateKey
 | |
|                     inCollection:TSAccountManager_UserAccountCollection];
 | |
| 
 | |
|     return [self updateAccountAttributesIfNecessary];
 | |
| }
 | |
| 
 | |
| - (AnyPromise *)updateAccountAttributesIfNecessary {
 | |
|     if (!self.isRegistered) {
 | |
|         return [AnyPromise promiseWithValue:@(1)];
 | |
|     }
 | |
| 
 | |
|     return [AnyPromise promiseWithValue:@(1)];
 | |
|     
 | |
|     NSDate *_Nullable updateRequestDate =
 | |
|         [self.dbConnection objectForKey:TSAccountManager_NeedsAccountAttributesUpdateKey
 | |
|                            inCollection:TSAccountManager_UserAccountCollection];
 | |
|     if (!updateRequestDate) {
 | |
|         return [AnyPromise promiseWithValue:@(1)];
 | |
|     }
 | |
|     AnyPromise *promise = [self performUpdateAccountAttributes];
 | |
|     promise = promise.then(^(id value) {
 | |
|         [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
 | |
|             // Clear the update request unless a new update has been requested
 | |
|             // while this update was in flight.
 | |
|             NSDate *_Nullable latestUpdateRequestDate =
 | |
|                 [transaction objectForKey:TSAccountManager_NeedsAccountAttributesUpdateKey
 | |
|                              inCollection:TSAccountManager_UserAccountCollection];
 | |
|             if (latestUpdateRequestDate && [latestUpdateRequestDate isEqual:updateRequestDate]) {
 | |
|                 [transaction removeObjectForKey:TSAccountManager_NeedsAccountAttributesUpdateKey
 | |
|                                    inCollection:TSAccountManager_UserAccountCollection];
 | |
|             }
 | |
|         }];
 | |
|     });
 | |
|     return promise;
 | |
| }
 | |
| 
 | |
| - (void)reachabilityChanged {
 | |
|     [AppReadiness runNowOrWhenAppDidBecomeReady:^{
 | |
|         [[self updateAccountAttributesIfNecessary] retainUntilComplete];
 | |
|     }];
 | |
| }
 | |
| 
 | |
| #pragma mark - Notifications
 | |
| 
 | |
| - (void)postRegistrationStateDidChangeNotification
 | |
| {
 | |
|     [[NSNotificationCenter defaultCenter] postNotificationNameAsync:RegistrationStateDidChangeNotification
 | |
|                                                              object:nil
 | |
|                                                            userInfo:nil];
 | |
| }
 | |
| 
 | |
| @end
 | |
| 
 | |
| NS_ASSUME_NONNULL_END
 |