|
@@ -1,12 +1,109 @@
|
|
|
#import "CocosIOSPlatformBridge.h"
|
|
#import "CocosIOSPlatformBridge.h"
|
|
|
|
|
+#import <Security/Security.h>
|
|
|
#import <UIKit/UIKit.h>
|
|
#import <UIKit/UIKit.h>
|
|
|
|
|
+#import <AppTrackingTransparency/AppTrackingTransparency.h>
|
|
|
|
|
|
|
|
@implementation CocosIOSPlatformBridge
|
|
@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 {
|
|
+ (nullable NSString *)getDeviceID {
|
|
|
- // Do not persist IDFV in backed-up storage: a restored backup can belong to
|
|
|
|
|
- // another device. Always obtain this device's identifier from the OS.
|
|
|
|
|
- return UIDevice.currentDevice.identifierForVendor.UUIDString;
|
|
|
|
|
|
|
+ @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
|
|
@end
|