// // FCUUID.m // // Created by Fabio Caccamo on 26/06/14. // Copyright © 2016 Fabio Caccamo. All rights reserved. // #import "FCUUID.h" #import "UICKeyChainStore.h" @implementation FCUUID NSString *const FCUUIDsOfUserDevicesDidChangeNotification = @"FCUUIDsOfUserDevicesDidChangeNotification"; NSString *const _uuidForInstallationKey = @"fc_uuidForInstallation"; NSString *const _uuidForDeviceKey = @"fc_uuidForDevice"; NSString *const _uuidsOfUserDevicesKey = @"fc_uuidsOfUserDevices"; NSString *const _uuidsOfUserDevicesToggleKey = @"fc_uuidsOfUserDevicesToggle"; +(FCUUID *)sharedInstance { static FCUUID *instance = nil; static dispatch_once_t token; dispatch_once(&token, ^{ instance = [[self alloc] init]; }); return instance; } -(instancetype)init { self = [super init]; if(self) { [self uuidsOfUserDevices_iCloudInit]; } return self; } -(NSString *)_getOrCreateValueForKey:(NSString *)key defaultValue:(NSString *)defaultValue userDefaults:(BOOL)userDefaults keychain:(BOOL)keychain service:(NSString *)service accessGroup:(NSString *)accessGroup synchronizable:(BOOL)synchronizable { NSString *value = [self _getValueForKey:key userDefaults:userDefaults keychain:keychain service:service accessGroup:accessGroup]; if(!value){ value = defaultValue; } if(!value){ value = [self uuid]; } [self _setValue:value forKey:key userDefaults:userDefaults keychain:keychain service:service accessGroup:accessGroup synchronizable:synchronizable]; return value; } -(NSString *)_getValueForKey:(NSString *)key userDefaults:(BOOL)userDefaults keychain:(BOOL)keychain service:(NSString *)service accessGroup:(NSString *)accessGroup { NSString *value = nil; if(!value && keychain ){ value = [UICKeyChainStore stringForKey:key service:service accessGroup:accessGroup]; } if(!value && userDefaults ){ value = [[NSUserDefaults standardUserDefaults] stringForKey:key]; } return value; } -(void)_setValue:(NSString *)value forKey:(NSString *)key userDefaults:(BOOL)userDefaults keychain:(BOOL)keychain service:(NSString *)service accessGroup:(NSString *)accessGroup synchronizable:(BOOL)synchronizable { if( value && userDefaults ){ [[NSUserDefaults standardUserDefaults] setObject:value forKey:key]; [[NSUserDefaults standardUserDefaults] synchronize]; } if( value && keychain ){ UICKeyChainStore *keychain = [UICKeyChainStore keyChainStoreWithService:service accessGroup:accessGroup]; [keychain setSynchronizable:synchronizable]; [keychain setString:value forKey:key]; } } -(NSString *)uuid { //also known as uuid/universallyUniqueIdentifier CFUUIDRef uuidRef = CFUUIDCreate(NULL); CFStringRef uuidStringRef = CFUUIDCreateString(NULL, uuidRef); CFRelease(uuidRef); NSString *uuidValue = (__bridge_transfer NSString *)uuidStringRef; uuidValue = [uuidValue lowercaseString]; uuidValue = [uuidValue stringByReplacingOccurrencesOfString:@"-" withString:@""]; return uuidValue; } -(NSString *)uuidForKey:(id)key { if( _uuidForKey == nil ){ _uuidForKey = [[NSMutableDictionary alloc] init]; } NSString *uuidValue = [_uuidForKey objectForKey:key]; if( uuidValue == nil ){ uuidValue = [self uuid]; [_uuidForKey setObject:uuidValue forKey:key]; } return uuidValue; } -(NSString *)uuidForSession { if( _uuidForSession == nil ){ _uuidForSession = [self uuid]; } return _uuidForSession; } -(NSString *)uuidForInstallation { if( _uuidForInstallation == nil ){ _uuidForInstallation = [self _getOrCreateValueForKey:_uuidForInstallationKey defaultValue:nil userDefaults:YES keychain:NO service:nil accessGroup:nil synchronizable:NO]; } return _uuidForInstallation; } -(NSString *)uuidForVendor { if( _uuidForVendor == nil ){ _uuidForVendor = [[[[[UIDevice currentDevice] identifierForVendor] UUIDString] lowercaseString] stringByReplacingOccurrencesOfString:@"-" withString:@""]; } return _uuidForVendor; } -(void)uuidForDevice_updateWithValue:(NSString *)value { _uuidForDevice = [NSString stringWithString:value]; [self _setValue:_uuidForDevice forKey:_uuidForDeviceKey userDefaults:YES keychain:YES service:nil accessGroup:nil synchronizable:NO]; } -(NSString *)uuidForDevice { //also known as udid/uniqueDeviceIdentifier but this doesn't persists to system reset if( _uuidForDevice == nil ){ _uuidForDevice = [self _getOrCreateValueForKey:_uuidForDeviceKey defaultValue:nil userDefaults:YES keychain:YES service:nil accessGroup:nil synchronizable:NO]; } return _uuidForDevice; } -(NSString *)uuidForDeviceMigratingValue:(NSString *)value commitMigration:(BOOL)commitMigration { if([self uuidValueIsValid:value]) { NSString *oldValue = [self uuidForDevice]; NSString *newValue = [NSString stringWithString:value]; if([oldValue isEqualToString:newValue]) { return oldValue; } if(commitMigration) { [self uuidForDevice_updateWithValue:newValue]; NSMutableOrderedSet *uuidsOfUserDevicesSet = [[NSMutableOrderedSet alloc] initWithArray:[self uuidsOfUserDevices]]; [uuidsOfUserDevicesSet addObject:newValue]; [uuidsOfUserDevicesSet removeObject:oldValue]; [self uuidsOfUserDevices_updateWithValue:[uuidsOfUserDevicesSet array]]; [self uuidsOfUserDevices_iCloudSync]; return [self uuidForDevice]; } else { return oldValue; } } else { [NSException raise:@"Invalid uuid to migrate" format:@"uuid value should be a string of 32 or 36 characters."]; return nil; } } -(NSString *)uuidForDeviceMigratingValueForKey:(NSString *)key commitMigration:(BOOL)commitMigration { return [self uuidForDeviceMigratingValueForKey:key service:nil accessGroup:nil commitMigration:commitMigration]; } -(NSString *)uuidForDeviceMigratingValueForKey:(NSString *)key service:(NSString *)service commitMigration:(BOOL)commitMigration { return [self uuidForDeviceMigratingValueForKey:key service:service accessGroup:nil commitMigration:commitMigration]; } -(NSString *)uuidForDeviceMigratingValueForKey:(NSString *)key service:(NSString *)service accessGroup:(NSString *)accessGroup commitMigration:(BOOL)commitMigration { NSString *uuidToMigrate = [self _getValueForKey:key userDefaults:YES keychain:YES service:service accessGroup:accessGroup]; return [self uuidForDeviceMigratingValue:uuidToMigrate commitMigration:commitMigration]; } -(void)uuidsOfUserDevices_iCloudInit { _uuidsOfUserDevices_iCloudAvailable = NO; if(NSClassFromString(@"NSUbiquitousKeyValueStore")) { NSUbiquitousKeyValueStore *iCloud = [NSUbiquitousKeyValueStore defaultStore]; if(iCloud) { _uuidsOfUserDevices_iCloudAvailable = YES; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(uuidsOfUserDevices_iCloudChange:) name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification object:nil]; [self uuidsOfUserDevices_iCloudSync]; } else { //NSLog(@"iCloud not available"); } } else { //NSLog(@"iOS < 5"); } } -(void)uuidsOfUserDevices_iCloudSync { if( _uuidsOfUserDevices_iCloudAvailable ) { NSUbiquitousKeyValueStore *iCloud = [NSUbiquitousKeyValueStore defaultStore]; //if keychain contains more device identifiers than icloud, maybe that icloud has been empty, so re-write these identifiers to iCloud for ( NSString *uuidOfUserDevice in [self uuidsOfUserDevices] ) { NSString *uuidOfUserDeviceAsKey = [NSString stringWithFormat:@"%@_%@", _uuidForDeviceKey, uuidOfUserDevice]; if(![[iCloud stringForKey:uuidOfUserDeviceAsKey] isEqualToString:uuidOfUserDevice]){ [iCloud setString:uuidOfUserDevice forKey:uuidOfUserDeviceAsKey]; } } //toggle a boolean value to force notification on other devices, useful for debug [iCloud setBool:![iCloud boolForKey:_uuidsOfUserDevicesToggleKey] forKey:_uuidsOfUserDevicesToggleKey]; [iCloud synchronize]; } } -(void)uuidsOfUserDevices_iCloudChange:(NSNotification *)notification { if( _uuidsOfUserDevices_iCloudAvailable ) { NSMutableOrderedSet *uuidsSet = [[NSMutableOrderedSet alloc] initWithArray:[self uuidsOfUserDevices]]; NSInteger uuidsCount = [uuidsSet count]; NSUbiquitousKeyValueStore *iCloud = [NSUbiquitousKeyValueStore defaultStore]; NSDictionary *iCloudDict = [iCloud dictionaryRepresentation]; //NSLog(@"uuidsOfUserDevicesSync: %@", iCloudDict); [iCloudDict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { NSString *uuidKey = (NSString *)key; if([uuidKey rangeOfString:_uuidForDeviceKey].location == 0) { if([obj isKindOfClass:[NSString class]]) { NSString *uuidValue = (NSString *)obj; if([uuidKey rangeOfString:uuidValue].location != NSNotFound && [self uuidValueIsValid:uuidValue]) { //NSLog(@"uuid: %@", uuidValue); [uuidsSet addObject:uuidValue]; } else { //NSLog(@"invalid uuid"); } } } }]; if([uuidsSet count] > uuidsCount) { [self uuidsOfUserDevices_updateWithValue:[uuidsSet array]]; NSDictionary *userInfo = [NSDictionary dictionaryWithObject:[self uuidsOfUserDevices] forKey:@"uuidsOfUserDevices"]; [[NSNotificationCenter defaultCenter] postNotificationName:FCUUIDsOfUserDevicesDidChangeNotification object:self userInfo:userInfo]; } } } -(void)uuidsOfUserDevices_updateWithValue:(NSArray *)value { _uuidsOfUserDevices = [value componentsJoinedByString:@"|"]; [self _setValue:_uuidsOfUserDevices forKey:_uuidsOfUserDevicesKey userDefaults:YES keychain:YES service:nil accessGroup:nil synchronizable:YES]; } -(NSArray *)uuidsOfUserDevices { if( _uuidsOfUserDevices == nil ){ _uuidsOfUserDevices = [self _getOrCreateValueForKey:_uuidsOfUserDevicesKey defaultValue:[self uuidForDevice] userDefaults:YES keychain:YES service:nil accessGroup:nil synchronizable:YES]; } return [_uuidsOfUserDevices componentsSeparatedByString:@"|"]; } -(NSArray *)uuidsOfUserDevicesExcludingCurrentDevice { NSMutableArray *uuids = [NSMutableArray arrayWithArray:[self uuidsOfUserDevices]]; [uuids removeObject:[self uuidForDevice]]; return [NSArray arrayWithArray:uuids]; } -(BOOL)uuidValueIsValid:(NSString *)uuidValue { if(uuidValue != nil) { NSString *uuidPattern = @"^[0-9a-f]{32}|[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$"; NSRegularExpression *uuidRegExp = [NSRegularExpression regularExpressionWithPattern:uuidPattern options:NSRegularExpressionCaseInsensitive error:nil]; NSRange uuidValueRange = NSMakeRange(0, [uuidValue length]); NSRange uuidMatchRange = [uuidRegExp rangeOfFirstMatchInString:uuidValue options:0 range:uuidValueRange]; NSString *uuidMatchValue; if(!NSEqualRanges(uuidMatchRange, NSMakeRange(NSNotFound, 0))) { uuidMatchValue = [uuidValue substringWithRange:uuidMatchRange]; if([uuidMatchValue isEqualToString:uuidValue]) { return YES; } else { return NO; } } else { return NO; } } else { return NO; } } +(NSString *)uuid { return [[self sharedInstance] uuid]; } +(NSString *)uuidForKey:(id)key { return [[self sharedInstance] uuidForKey:key]; } +(NSString *)uuidForSession { return [[self sharedInstance] uuidForSession]; } +(NSString *)uuidForInstallation { return [[self sharedInstance] uuidForInstallation]; } +(NSString *)uuidForVendor { return [[self sharedInstance] uuidForVendor]; } +(NSString *)uuidForDevice { return [[self sharedInstance] uuidForDevice]; } +(NSString *)uuidForDeviceMigratingValue:(NSString *)value commitMigration:(BOOL)commitMigration { return [[self sharedInstance] uuidForDeviceMigratingValue:value commitMigration:commitMigration]; } +(NSString *)uuidForDeviceMigratingValueForKey:(NSString *)key commitMigration:(BOOL)commitMigration { return [[self sharedInstance] uuidForDeviceMigratingValueForKey:key service:nil accessGroup:nil commitMigration:commitMigration]; } +(NSString *)uuidForDeviceMigratingValueForKey:(NSString *)key service:(NSString *)service commitMigration:(BOOL)commitMigration { return [[self sharedInstance] uuidForDeviceMigratingValueForKey:key service:service accessGroup:nil commitMigration:commitMigration]; } +(NSString *)uuidForDeviceMigratingValueForKey:(NSString *)key service:(NSString *)service accessGroup:(NSString *)accessGroup commitMigration:(BOOL)commitMigration { return [[self sharedInstance] uuidForDeviceMigratingValueForKey:key service:service accessGroup:accessGroup commitMigration:commitMigration]; } +(NSArray *)uuidsOfUserDevices { return [[self sharedInstance] uuidsOfUserDevices]; } +(NSArray *)uuidsOfUserDevicesExcludingCurrentDevice { return [[self sharedInstance] uuidsOfUserDevicesExcludingCurrentDevice]; } +(BOOL)uuidValueIsValid:(NSString *)uuidValue { return [[self sharedInstance] uuidValueIsValid:uuidValue]; } @end