#import "CocosIOSPlatformBridge.h" #import #import #import @implementation CocosIOSPlatformBridge + (void)requestTrackingAuthorization { dispatch_async(dispatch_get_main_queue(), ^{ // All accesses are serialized on the main queue. static BOOL requestInFlight = NO; static id activeObserver = nil; if (@available(iOS 14, *)) { if (requestInFlight) return; NSString *usage = [NSBundle.mainBundle objectForInfoDictionaryKey:@"NSUserTrackingUsageDescription"]; if (![usage isKindOfClass:NSString.class] || !usage.length) { NSLog(@"[ATT] Missing NSUserTrackingUsageDescription; request skipped."); return; } if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive) { if (!activeObserver) { activeObserver = [NSNotificationCenter.defaultCenter addObserverForName:UIApplicationDidBecomeActiveNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification *notification) { [NSNotificationCenter.defaultCenter removeObserver:activeObserver]; activeObserver = nil; [CocosIOSPlatformBridge requestTrackingAuthorization]; }]; } return; } if (activeObserver) { [NSNotificationCenter.defaultCenter removeObserver:activeObserver]; activeObserver = nil; } ATTrackingManagerAuthorizationStatus status = ATTrackingManager.trackingAuthorizationStatus; if (status != ATTrackingManagerAuthorizationStatusNotDetermined) { NSLog(@"[ATT] Existing status: %ld", (long)status); return; } requestInFlight = YES; [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus result) { dispatch_async(dispatch_get_main_queue(), ^{ requestInFlight = NO; // 0=notDetermined, 1=restricted, 2=denied, 3=authorized. NSLog(@"[ATT] Authorization status: %ld", (long)result); }); }]; } else { NSLog(@"[ATT] Not available before iOS 14."); } }); } + (nullable NSString *)getDeviceID { @synchronized(self) { NSString *bundleID = NSBundle.mainBundle.bundleIdentifier; if (!bundleID.length) return nil; // Keep these keys stable between releases to preserve the identifier. NSDictionary *key = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrService: [bundleID stringByAppendingString:@".device-identifier"], (__bridge id)kSecAttrAccount: @"deviceUUID.v1", (__bridge id)kSecAttrSynchronizable: @NO, }; // Retry a read if another process created the item between our read/add. for (NSUInteger attempt = 0; attempt < 2; ++attempt) { NSMutableDictionary *query = [key mutableCopy]; query[(__bridge id)kSecReturnData] = @YES; query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne; CFTypeRef result = NULL; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result); id data = CFBridgingRelease(result); if (status == errSecSuccess) { NSString *savedUUID = [data isKindOfClass:NSData.class] ? [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] : nil; if (savedUUID.length && [[NSUUID alloc] initWithUUIDString:savedUUID]) { return savedUUID; } // A damaged value must not silently replace an existing identity. NSLog(@"[iOS DeviceID] Invalid Keychain UUID; keeping the existing item."); return nil; } if (status != errSecItemNotFound) { // Includes a locked/unavailable Keychain. Do not create a new ID. NSLog(@"[iOS DeviceID] Keychain read failed: %d", (int)status); return nil; } NSString *uuid = NSUUID.UUID.UUIDString; NSMutableDictionary *item = [key mutableCopy]; item[(__bridge id)kSecValueData] = [uuid dataUsingEncoding:NSUTF8StringEncoding]; // No iCloud sync or migration to another device through backups. item[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly; status = SecItemAdd((__bridge CFDictionaryRef)item, NULL); if (status == errSecSuccess) return uuid; if (status != errSecDuplicateItem) { NSLog(@"[iOS DeviceID] Keychain write failed: %d", (int)status); return nil; } } NSLog(@"[iOS DeviceID] Keychain item changed concurrently; retry on next call."); return nil; } } @end