diff --git a/GoogleUtilities.podspec b/GoogleUtilities.podspec index 577c03d8ef1..0e22e9892c7 100644 --- a/GoogleUtilities.podspec +++ b/GoogleUtilities.podspec @@ -90,4 +90,11 @@ other Google CocoaPods. They're not intended for direct public usage. sths.source_files = 'GoogleUtilities/SwizzlerTestHelpers/*.[hm]' sths.private_header_files = 'GoogleUtilities/SwizzlerTestHelpers/*.h' end + + s.subspec 'UserDefaults' do |ud| + ud.source_files = 'GoogleUtilities/UserDefaults/**/*.[hm]' + ud.public_header_files = 'GoogleUtilities/UserDefaults/Private/*.h' + ud.private_header_files = 'GoogleUtilities/UserDefaults/Private/*.h' + ud.dependency 'GoogleUtilities/Logger' + end end diff --git a/GoogleUtilities/Example/GoogleUtilities.xcodeproj/project.pbxproj b/GoogleUtilities/Example/GoogleUtilities.xcodeproj/project.pbxproj index 62b6005af78..8307b1c52d1 100644 --- a/GoogleUtilities/Example/GoogleUtilities.xcodeproj/project.pbxproj +++ b/GoogleUtilities/Example/GoogleUtilities.xcodeproj/project.pbxproj @@ -46,6 +46,9 @@ DEC9788720F6E1E000014E20 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DEC9788020F6E1DF00014E20 /* Main.storyboard */; }; DEC9788820F6E1E000014E20 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = DEC9788120F6E1DF00014E20 /* main.m */; }; DEC9788920F6E1E000014E20 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = DEC9788220F6E1DF00014E20 /* AppDelegate.m */; }; + ED18C2A0213EDB98009F633D /* GULUserDefaultsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ED18C29F213EDB98009F633D /* GULUserDefaultsTests.m */; }; + ED18C2A1213EDB98009F633D /* GULUserDefaultsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ED18C29F213EDB98009F633D /* GULUserDefaultsTests.m */; }; + ED18C2A2213EDB98009F633D /* GULUserDefaultsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ED18C29F213EDB98009F633D /* GULUserDefaultsTests.m */; }; EFBE67FA2101401100E756A7 /* GULSwizzlerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = EFBE67F02101401100E756A7 /* GULSwizzlerTest.m */; }; EFBE67FB2101401100E756A7 /* GULSwizzlingCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = EFBE67F12101401100E756A7 /* GULSwizzlingCacheTest.m */; }; EFBE67FC2101401100E756A7 /* GULRuntimeClassDiffTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EFBE67F22101401100E756A7 /* GULRuntimeClassDiffTests.m */; }; @@ -126,6 +129,7 @@ DEC9788320F6E1DF00014E20 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DEC9788420F6E1DF00014E20 /* ViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; E0A8D570636E99E7C3396DF8 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; + ED18C29F213EDB98009F633D /* GULUserDefaultsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GULUserDefaultsTests.m; sourceTree = ""; }; EFBE67F02101401100E756A7 /* GULSwizzlerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GULSwizzlerTest.m; sourceTree = ""; }; EFBE67F12101401100E756A7 /* GULSwizzlingCacheTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GULSwizzlingCacheTest.m; sourceTree = ""; }; EFBE67F22101401100E756A7 /* GULRuntimeClassDiffTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GULRuntimeClassDiffTests.m; sourceTree = ""; }; @@ -231,6 +235,7 @@ 6003F5B5195388D20070C39A /* Tests */ = { isa = PBXGroup; children = ( + ED18C29E213ED7B9009F633D /* User Defaults */, EFBE67EF2101401100E756A7 /* Swizzler */, DEC977DE20F6A7A700014E20 /* Logger */, DEC977D420F68C3300014E20 /* Network */, @@ -347,6 +352,14 @@ path = tvOS; sourceTree = ""; }; + ED18C29E213ED7B9009F633D /* User Defaults */ = { + isa = PBXGroup; + children = ( + ED18C29F213EDB98009F633D /* GULUserDefaultsTests.m */, + ); + path = "User Defaults"; + sourceTree = ""; + }; EFBE67EF2101401100E756A7 /* Swizzler */ = { isa = PBXGroup; children = ( @@ -597,6 +610,7 @@ EFBE67FF2101401100E756A7 /* GULRuntimeDiffTests.m in Sources */, DEC977DD20F68FE100014E20 /* GTMHTTPServer.m in Sources */, EFBE68022101401100E756A7 /* GULRuntimeSnapshotTests.m in Sources */, + ED18C2A0213EDB98009F633D /* GULUserDefaultsTests.m in Sources */, EFBE68002101401100E756A7 /* GULSwizzlerInheritedMethodsSwizzlingTest.m in Sources */, EFBE68012101401100E756A7 /* GULRuntimeStateHelperTests.m in Sources */, DEC977D820F68C3300014E20 /* GULMutableDictionaryTest.m in Sources */, @@ -629,6 +643,7 @@ DEC9781920F6D38500014E20 /* GULAppEnvironmentUtilTest.m in Sources */, DEC9781A20F6D38800014E20 /* GULReachabilityCheckerTest.m in Sources */, DEC9781820F6D37400014E20 /* GULLoggerTest.m in Sources */, + ED18C2A1213EDB98009F633D /* GULUserDefaultsTests.m in Sources */, DEC9781D20F6D39900014E20 /* GTMHTTPServer.m in Sources */, DEC9781B20F6D39500014E20 /* GULMutableDictionaryTest.m in Sources */, DEC9781C20F6D39500014E20 /* GULNetworkTest.m in Sources */, @@ -652,6 +667,7 @@ DEC9786920F6D66300014E20 /* GTMHTTPServer.m in Sources */, DEC9786B20F6D66300014E20 /* GULNetworkTest.m in Sources */, DEC9786A20F6D66300014E20 /* GULMutableDictionaryTest.m in Sources */, + ED18C2A2213EDB98009F633D /* GULUserDefaultsTests.m in Sources */, DEC9786C20F6D66700014E20 /* GULReachabilityCheckerTest.m in Sources */, DEC9786820F6D65B00014E20 /* GULLoggerTest.m in Sources */, DEC9786D20F6D66B00014E20 /* GULAppEnvironmentUtilTest.m in Sources */, diff --git a/GoogleUtilities/Example/Tests/User Defaults/GULUserDefaultsTests.m b/GoogleUtilities/Example/Tests/User Defaults/GULUserDefaultsTests.m new file mode 100644 index 00000000000..e38d2741695 --- /dev/null +++ b/GoogleUtilities/Example/Tests/User Defaults/GULUserDefaultsTests.m @@ -0,0 +1,846 @@ +// Copyright 2018 Google +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import + +#import +#import + +static const double sEpsilon = 0.001; + +/// The maximum time to wait for an expectation before failing. +static const NSTimeInterval kGULTestCaseTimeoutInterval = 10; + +@interface GULUserDefaults () +// Expose for testing. +- (void)clearAllData; +@end + +@interface GULUserDefaultsThreadArgs : NSObject + +/// The new user defaults to be tested on threads. +@property(atomic) GULUserDefaults *userDefaults; + +/// The old user defaults to be tested on threads. +@property(atomic) NSUserDefaults *oldUserDefaults; + +/// The thread index. +@property(atomic) int index; + +/// The number of items to be removed/added into the dictionary per thread. +@property(atomic) int itemsPerThread; + +/// The dictionary that store all the objects that the user defaults stores. +@property(atomic) GULMutableDictionary *dictionary; + +@end + +@implementation GULUserDefaultsThreadArgs +@end + +@interface GULUserDefaultsTests : XCTestCase + +@end + +@implementation GULUserDefaultsTests + +- (void)testNewUserDefaultsWithStandardUserDefaults { + NSString *suiteName = @"test_suite_defaults"; + NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName]; + GULUserDefaults *newUserDefaults = [[GULUserDefaults alloc] initWithSuiteName:suiteName]; + + NSString *key1 = @"testing"; + NSString *value1 = @"blabla"; + [newUserDefaults setObject:value1 forKey:key1]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key1], @"blabla"); + XCTAssertEqualObjects([userDefaults objectForKey:key1], @"blabla"); + XCTAssertEqualObjects([newUserDefaults stringForKey:key1], @"blabla"); + + NSString *key2 = @"OtherKey"; + NSNumber *number = @(123.45); + [newUserDefaults setDouble:123.45 forKey:key2]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key2], number); + XCTAssertEqualWithAccuracy([newUserDefaults doubleForKey:key2], 123.45, sEpsilon); + XCTAssertEqualObjects([userDefaults objectForKey:key2], number); + + NSString *key3 = @"ArrayKey"; + NSArray *array = @[ @1, @"Hi" ]; + [newUserDefaults setObject:array forKey:key3]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key3], array); + XCTAssertEqualObjects([newUserDefaults arrayForKey:key3], array); + XCTAssertEqualObjects([userDefaults objectForKey:key3], array); + + NSString *key4 = @"DictionaryKey"; + NSDictionary *dictionary = @{ @"testing" : @"Hi there!" }; + [newUserDefaults setObject:dictionary forKey:key4]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key4], dictionary); + XCTAssertEqualObjects([newUserDefaults dictionaryForKey:key4], dictionary); + XCTAssertEqualObjects([userDefaults objectForKey:key4], dictionary); + + NSString *key5 = @"BoolKey"; + NSNumber *boolObject = @(YES); + XCTAssertFalse([newUserDefaults boolForKey:key5]); + XCTAssertFalse([userDefaults boolForKey:key5]); + [newUserDefaults setObject:boolObject forKey:key5]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key5], boolObject); + XCTAssertEqualObjects([userDefaults objectForKey:key5], boolObject); + XCTAssertTrue([newUserDefaults boolForKey:key5]); + XCTAssertTrue([userDefaults boolForKey:key5]); + [newUserDefaults setBool:NO forKey:key5]; + XCTAssertFalse([newUserDefaults boolForKey:key5]); + XCTAssertFalse([userDefaults boolForKey:key5]); + + NSString *key6 = @"DataKey"; + NSData *testData = [@"google" dataUsingEncoding:NSUTF8StringEncoding]; + [newUserDefaults setObject:testData forKey:key6]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key6], testData); + XCTAssertEqualObjects([userDefaults objectForKey:key6], testData); + + NSString *key7 = @"DateKey"; + NSDate *testDate = [NSDate date]; + [newUserDefaults setObject:testDate forKey:key7]; + XCTAssertNotNil([newUserDefaults objectForKey:key7]); + XCTAssertNotNil([userDefaults objectForKey:key7]); + XCTAssertEqualWithAccuracy([testDate timeIntervalSinceDate:[newUserDefaults objectForKey:key7]], + 0.0, sEpsilon); + XCTAssertEqualWithAccuracy([testDate timeIntervalSinceDate:[userDefaults objectForKey:key7]], + 0.0, sEpsilon); + + NSString *key8 = @"FloatKey"; + [newUserDefaults setFloat:0.99 forKey:key8]; + XCTAssertEqualWithAccuracy([newUserDefaults floatForKey:key8], 0.99, sEpsilon); + XCTAssertEqualWithAccuracy([userDefaults floatForKey:key8], 0.99, sEpsilon); + + // Remove all of the objects from the normal NSUserDefaults. The values from the new user + // defaults must also be cleared! + [userDefaults removePersistentDomainForName:suiteName]; + XCTAssertNil([userDefaults objectForKey:key1]); + XCTAssertNil([newUserDefaults objectForKey:key1]); + XCTAssertNil([userDefaults objectForKey:key2]); + XCTAssertNil([newUserDefaults objectForKey:key2]); + + [newUserDefaults setObject:@"anothervalue" forKey:key1]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key1], @"anothervalue"); + XCTAssertEqualObjects([userDefaults objectForKey:key1], @"anothervalue"); + + [newUserDefaults setInteger:111 forKey:key2]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key2], @111); + XCTAssertEqual([newUserDefaults integerForKey:key2], 111); + XCTAssertEqualObjects([userDefaults objectForKey:key2], @111); + + NSArray *array2 = @[ @2, @"Hello" ]; + [newUserDefaults setObject:array2 forKey:key3]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key3], array2); + XCTAssertEqualObjects([newUserDefaults arrayForKey:key3], array2); + XCTAssertEqualObjects([userDefaults objectForKey:key3], array2); + + NSDictionary *dictionary2 = @{ @"testing 2" : @3 }; + [newUserDefaults setObject:dictionary2 forKey:key4]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key4], dictionary2); + XCTAssertEqualObjects([newUserDefaults dictionaryForKey:key4], dictionary2); + XCTAssertEqualObjects([userDefaults objectForKey:key4], dictionary2); + + // Remove all of the objects from the new user defaults. The values from the NSUserDefaults must + // also be cleared. + [newUserDefaults clearAllData]; + XCTAssertNil([userDefaults objectForKey:key1]); + XCTAssertNil([newUserDefaults objectForKey:key1]); + XCTAssertNil([userDefaults objectForKey:key2]); + XCTAssertNil([newUserDefaults objectForKey:key2]); + XCTAssertNil([userDefaults objectForKey:key3]); + XCTAssertNil([newUserDefaults objectForKey:key3]); + XCTAssertNil([userDefaults objectForKey:key4]); + XCTAssertNil([newUserDefaults objectForKey:key4]); + XCTAssertNil([userDefaults objectForKey:key5]); + XCTAssertNil([newUserDefaults objectForKey:key5]); + XCTAssertNil([userDefaults objectForKey:key6]); + XCTAssertNil([newUserDefaults objectForKey:key6]); + XCTAssertNil([userDefaults objectForKey:key7]); + XCTAssertNil([newUserDefaults objectForKey:key7]); + XCTAssertNil([userDefaults objectForKey:key8]); + XCTAssertNil([newUserDefaults objectForKey:key8]); + + [self removePreferenceFileWithSuiteName:suiteName]; +} + +- (void)testNSUserDefaultsWithNewUserDefaults { + NSString *suiteName = @"test_suite_defaults_2"; + NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName]; + GULUserDefaults *newUserDefaults = [[GULUserDefaults alloc] initWithSuiteName:suiteName]; + + NSString *key1 = @"testing"; + NSString *value1 = @"blabla"; + [userDefaults setObject:value1 forKey:key1]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key1], @"blabla"); + XCTAssertEqualObjects([userDefaults objectForKey:key1], @"blabla"); + XCTAssertEqualObjects([newUserDefaults stringForKey:key1], @"blabla"); + + NSString *key2 = @"OtherKey"; + NSNumber *number = @(123.45); + [userDefaults setDouble:123.45 forKey:key2]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key2], number); + XCTAssertEqualWithAccuracy([newUserDefaults doubleForKey:key2], 123.45, sEpsilon); + XCTAssertEqualObjects([userDefaults objectForKey:key2], number); + + NSString *key3 = @"ArrayKey"; + NSArray *array = @[ @1, @"Hi" ]; + [userDefaults setObject:array forKey:key3]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key3], array); + XCTAssertEqualObjects([newUserDefaults arrayForKey:key3], array); + XCTAssertEqualObjects([userDefaults objectForKey:key3], array); + + NSString *key4 = @"DictionaryKey"; + NSDictionary *dictionary = @{ @"testing" : @"Hi there!" }; + [userDefaults setObject:dictionary forKey:key4]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key4], dictionary); + XCTAssertEqualObjects([newUserDefaults dictionaryForKey:key4], dictionary); + XCTAssertEqualObjects([userDefaults objectForKey:key4], dictionary); + + NSString *key5 = @"BoolKey"; + NSNumber *boolObject = @(YES); + XCTAssertFalse([newUserDefaults boolForKey:key5]); + XCTAssertFalse([userDefaults boolForKey:key5]); + [userDefaults setObject:boolObject forKey:key5]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key5], boolObject); + XCTAssertEqualObjects([userDefaults objectForKey:key5], boolObject); + XCTAssertTrue([newUserDefaults boolForKey:key5]); + XCTAssertTrue([userDefaults boolForKey:key5]); + [userDefaults setObject:@(NO) forKey:key5]; + XCTAssertFalse([newUserDefaults boolForKey:key5]); + XCTAssertFalse([userDefaults boolForKey:key5]); + + NSString *key6 = @"DataKey"; + NSData *testData = [@"google" dataUsingEncoding:NSUTF8StringEncoding]; + [userDefaults setObject:testData forKey:key6]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key6], testData); + XCTAssertEqualObjects([userDefaults objectForKey:key6], testData); + + NSString *key7 = @"DateKey"; + NSDate *testDate = [NSDate date]; + [userDefaults setObject:testDate forKey:key7]; + XCTAssertNotNil([newUserDefaults objectForKey:key7]); + XCTAssertNotNil([userDefaults objectForKey:key7]); + XCTAssertEqualWithAccuracy([testDate timeIntervalSinceDate:[newUserDefaults objectForKey:key7]], + 0.0, sEpsilon); + XCTAssertEqualWithAccuracy([testDate timeIntervalSinceDate:[userDefaults objectForKey:key7]], + 0.0, sEpsilon); + + NSString *key8 = @"FloatKey"; + [userDefaults setFloat:0.99 forKey:key8]; + XCTAssertEqualWithAccuracy([newUserDefaults floatForKey:key8], 0.99, sEpsilon); + XCTAssertEqualWithAccuracy([userDefaults floatForKey:key8], 0.99, sEpsilon); + + // Remove all of the objects from the normal NSUserDefaults. The values from the new user + // defaults must also be cleared! + [userDefaults removePersistentDomainForName:suiteName]; + XCTAssertNil([userDefaults objectForKey:key1]); + XCTAssertNil([newUserDefaults objectForKey:key1]); + XCTAssertNil([userDefaults objectForKey:key2]); + XCTAssertNil([newUserDefaults objectForKey:key2]); + + [userDefaults setObject:@"anothervalue" forKey:key1]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key1], @"anothervalue"); + XCTAssertEqualObjects([userDefaults objectForKey:key1], @"anothervalue"); + + [userDefaults setObject:@111 forKey:key2]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key2], @111); + XCTAssertEqual([newUserDefaults integerForKey:key2], 111); + XCTAssertEqualObjects([userDefaults objectForKey:key2], @111); + + NSArray *array2 = @[ @2, @"Hello" ]; + [userDefaults setObject:array2 forKey:key3]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key3], array2); + XCTAssertEqualObjects([newUserDefaults arrayForKey:key3], array2); + XCTAssertEqualObjects([userDefaults objectForKey:key3], array2); + + NSDictionary *dictionary2 = @{ @"testing 2" : @3 }; + [userDefaults setObject:dictionary2 forKey:key4]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key4], dictionary2); + XCTAssertEqualObjects([newUserDefaults dictionaryForKey:key4], dictionary2); + XCTAssertEqualObjects([userDefaults objectForKey:key4], dictionary2); + + // Remove all of the objects from the new user defaults. The values from the NSUserDefaults must + // also be cleared. + [userDefaults removePersistentDomainForName:suiteName]; + XCTAssertNil([userDefaults objectForKey:key1]); + XCTAssertNil([newUserDefaults objectForKey:key1]); + XCTAssertNil([userDefaults objectForKey:key2]); + XCTAssertNil([newUserDefaults objectForKey:key2]); + XCTAssertNil([userDefaults objectForKey:key3]); + XCTAssertNil([newUserDefaults objectForKey:key3]); + XCTAssertNil([userDefaults objectForKey:key4]); + XCTAssertNil([newUserDefaults objectForKey:key4]); + XCTAssertNil([userDefaults objectForKey:key5]); + XCTAssertNil([newUserDefaults objectForKey:key5]); + XCTAssertNil([userDefaults objectForKey:key6]); + XCTAssertNil([newUserDefaults objectForKey:key6]); + XCTAssertNil([userDefaults objectForKey:key7]); + XCTAssertNil([newUserDefaults objectForKey:key7]); + XCTAssertNil([userDefaults objectForKey:key8]); + XCTAssertNil([newUserDefaults objectForKey:key8]); + + [self removePreferenceFileWithSuiteName:suiteName]; +} + +- (void)testNewSharedUserDefaultsWithStandardUserDefaults { + NSString *appDomain = [NSBundle mainBundle].bundleIdentifier; + NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; + GULUserDefaults *newUserDefaults = [GULUserDefaults standardUserDefaults]; + + NSString *key1 = @"testing"; + NSString *value1 = @"blabla"; + [newUserDefaults setObject:value1 forKey:key1]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key1], @"blabla"); + XCTAssertEqualObjects([userDefaults objectForKey:key1], @"blabla"); + XCTAssertEqualObjects([newUserDefaults stringForKey:key1], @"blabla"); + + NSString *key2 = @"OtherKey"; + NSNumber *number = @(123.45); + [newUserDefaults setObject:number forKey:key2]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key2], number); + XCTAssertEqualWithAccuracy([newUserDefaults doubleForKey:key2], 123.45, sEpsilon); + XCTAssertEqualWithAccuracy([newUserDefaults floatForKey:key2], 123.45, sEpsilon); + XCTAssertEqualObjects([userDefaults objectForKey:key2], number); + + NSString *key3 = @"ArrayKey"; + NSArray *array = @[ @1, @"Hi" ]; + [userDefaults setObject:array forKey:key3]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key3], array); + XCTAssertEqualObjects([newUserDefaults arrayForKey:key3], array); + XCTAssertEqualObjects([userDefaults objectForKey:key3], array); + + NSString *key4 = @"DictionaryKey"; + NSDictionary *dictionary = @{ @"testing" : @"Hi there!" }; + [userDefaults setObject:dictionary forKey:key4]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key4], dictionary); + XCTAssertEqualObjects([newUserDefaults dictionaryForKey:key4], dictionary); + XCTAssertEqualObjects([userDefaults objectForKey:key4], dictionary); + + NSString *key5 = @"BoolKey"; + NSNumber *boolObject = @(1); + XCTAssertFalse([newUserDefaults boolForKey:key5]); + XCTAssertFalse([userDefaults boolForKey:key5]); + [userDefaults setObject:boolObject forKey:key5]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key5], boolObject); + XCTAssertEqualObjects([userDefaults objectForKey:key5], boolObject); + XCTAssertTrue([newUserDefaults boolForKey:key5]); + XCTAssertTrue([userDefaults boolForKey:key5]); + [userDefaults setObject:@(0) forKey:key5]; + XCTAssertFalse([newUserDefaults boolForKey:key5]); + XCTAssertFalse([userDefaults boolForKey:key5]); + + NSString *key6 = @"DataKey"; + NSData *testData = [@"google" dataUsingEncoding:NSUTF8StringEncoding]; + [newUserDefaults setObject:testData forKey:key6]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key6], testData); + XCTAssertEqualObjects([userDefaults objectForKey:key6], testData); + + NSString *key7 = @"DateKey"; + NSDate *testDate = [NSDate date]; + [newUserDefaults setObject:testDate forKey:key7]; + XCTAssertNotNil([newUserDefaults objectForKey:key7]); + XCTAssertNotNil([userDefaults objectForKey:key7]); + XCTAssertEqualWithAccuracy([testDate timeIntervalSinceDate:[newUserDefaults objectForKey:key7]], + 0.0, sEpsilon); + XCTAssertEqualWithAccuracy([testDate timeIntervalSinceDate:[userDefaults objectForKey:key7]], + 0.0, sEpsilon); + + // Remove all of the objects from the normal NSUserDefaults. The values from the new user + // defaults must also be cleared! + [[NSUserDefaults standardUserDefaults] removePersistentDomainForName:appDomain]; + XCTAssertNil([userDefaults objectForKey:key1]); + XCTAssertNil([newUserDefaults objectForKey:key1]); + XCTAssertNil([userDefaults objectForKey:key2]); + XCTAssertNil([newUserDefaults objectForKey:key2]); + + [userDefaults setObject:@"anothervalue" forKey:key1]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key1], @"anothervalue"); + XCTAssertEqualObjects([userDefaults objectForKey:key1], @"anothervalue"); + + [userDefaults setObject:@111 forKey:key2]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key2], @111); + XCTAssertEqual([newUserDefaults integerForKey:key2], 111); + XCTAssertEqualObjects([userDefaults objectForKey:key2], @111); + + NSArray *array2 = @[ @2, @"Hello" ]; + [userDefaults setObject:array2 forKey:key3]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key3], array2); + XCTAssertEqualObjects([newUserDefaults arrayForKey:key3], array2); + XCTAssertEqualObjects([userDefaults objectForKey:key3], array2); + + NSDictionary *dictionary2 = @{ @"testing 2" : @3 }; + [userDefaults setObject:dictionary2 forKey:key4]; + XCTAssertEqualObjects([newUserDefaults objectForKey:key4], dictionary2); + XCTAssertEqualObjects([userDefaults objectForKey:key4], dictionary2); + + // Remove all of the objects from the new user defaults. The values from the NSUserDefaults must + // also be cleared. + [newUserDefaults clearAllData]; + XCTAssertNil([userDefaults objectForKey:key1]); + XCTAssertNil([newUserDefaults objectForKey:key1]); + XCTAssertNil([userDefaults objectForKey:key2]); + XCTAssertNil([newUserDefaults objectForKey:key2]); + XCTAssertNil([userDefaults objectForKey:key3]); + XCTAssertNil([newUserDefaults objectForKey:key3]); + XCTAssertNil([userDefaults objectForKey:key4]); + XCTAssertNil([newUserDefaults objectForKey:key4]); + XCTAssertNil([userDefaults objectForKey:key5]); + XCTAssertNil([newUserDefaults objectForKey:key5]); + XCTAssertNil([userDefaults objectForKey:key6]); + XCTAssertNil([newUserDefaults objectForKey:key6]); + XCTAssertNil([userDefaults objectForKey:key7]); + XCTAssertNil([newUserDefaults objectForKey:key7]); + + [[NSUserDefaults standardUserDefaults] removePersistentDomainForName:appDomain]; +} + +- (void)testUserDefaultNotifications { + // Test to ensure no notifications are sent with our implementation. + void (^callBlock)(NSNotification *) = ^(NSNotification *_Nonnull notification) { + XCTFail(@"A notification must not be sent for GULUserDefaults!"); + }; + + id observer = + [[NSNotificationCenter defaultCenter] addObserverForName:NSUserDefaultsDidChangeNotification + object:nil + queue:nil + usingBlock:callBlock]; + NSString *suiteName = @"test_suite_notification"; + GULUserDefaults *newUserDefaults = [[GULUserDefaults alloc] initWithSuiteName:suiteName]; + [newUserDefaults setObject:@"134" forKey:@"test-another"]; + XCTAssertEqualObjects([newUserDefaults objectForKey:@"test-another"], @"134"); + [newUserDefaults setObject:nil forKey:@"test-another"]; + XCTAssertNil([newUserDefaults objectForKey:@"test-another"]); + [newUserDefaults synchronize]; + [newUserDefaults clearAllData]; + [[NSNotificationCenter defaultCenter] removeObserver:observer]; + + // Remove the underlying reference file. + [self removePreferenceFileWithSuiteName:suiteName]; +} + +- (void)testSynchronizeToDisk { +#if TARGET_OS_MAC + // `NSFileManager` has trouble reading the files in `~/Library` even though the + // `removeItemAtPath:` call works. Watching Finder while stepping through this test shows that the + // file does get created and removed properly. When using LLDB to call `fileExistsAtPath:` the + // correct return value of `YES` is returned, but in this test it returns `NO`. Best guess is the + // test app is sandboxed and `NSFileManager` is refusing to read the directory. + // TODO: Investigate the failure and re-enable this test. + return; +#endif // TARGET_OS_MAC + NSString *suiteName = [NSString stringWithFormat:@"another_test_suite"]; + NSString *filePath = [self filePathForPreferencesName:suiteName]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + + // Test the new User Defaults. + [fileManager removeItemAtPath:filePath error:NULL]; + XCTAssertFalse([fileManager fileExistsAtPath:filePath]); + + GULUserDefaults *newUserDefaults = [[GULUserDefaults alloc] initWithSuiteName:suiteName]; + [newUserDefaults setObject:@"134" forKey:@"test-another"]; + [newUserDefaults synchronize]; + + XCTAssertTrue([fileManager fileExistsAtPath:filePath], @"The user defaults file was not synchronized to disk."); + + // Now get the file directly from disk. + XCTAssertTrue([fileManager fileExistsAtPath:filePath]); + [newUserDefaults clearAllData]; + [newUserDefaults synchronize]; + + [self removePreferenceFileWithSuiteName:suiteName]; +} + +- (void)testInvalidKeys { + NSString *suiteName = @"test_suite_invalid_key"; + GULUserDefaults *newUserDefaults = [[GULUserDefaults alloc] initWithSuiteName:suiteName]; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + // These mostly to make sure that we don't crash. + [newUserDefaults setObject:@"test" forKey:nil]; + [newUserDefaults setObject:@"test" forKey:(NSString *)@123]; + [newUserDefaults setObject:@"test" forKey:@""]; + [newUserDefaults objectForKey:@""]; + [newUserDefaults objectForKey:(NSString *)@123]; + [newUserDefaults objectForKey:nil]; +#pragma clang diagnostic pop + + [self removePreferenceFileWithSuiteName:suiteName]; +} + +- (void)testInvalidObjects { + NSString *suiteName = @"test_suite_invalid_obj"; + GULUserDefaults *newUserDefaults = [[GULUserDefaults alloc] initWithSuiteName:suiteName]; + + GULMutableDictionary *invalidObject = [[GULMutableDictionary alloc] init]; + [newUserDefaults setObject:invalidObject forKey:@"Key"]; + XCTAssertNil([newUserDefaults objectForKey:@"Key"]); + [self removePreferenceFileWithSuiteName:suiteName]; +} + +- (void)testSetNilObject { + NSString *suiteName = @"test_suite_set_nil"; + GULUserDefaults *newUserDefaults = [[GULUserDefaults alloc] initWithSuiteName:suiteName]; + [newUserDefaults setObject:@"blabla" forKey:@"fine"]; + XCTAssertEqualObjects([newUserDefaults objectForKey:@"fine"], @"blabla"); + + [newUserDefaults setObject:nil forKey:@"fine"]; + XCTAssertNil([newUserDefaults objectForKey:@"fine"]); + [newUserDefaults clearAllData]; + + [self removePreferenceFileWithSuiteName:suiteName]; +} + +- (void)testRemoveObject { + NSString *suiteName = @"test_suite_remove"; + GULUserDefaults *newUserDefaults = [[GULUserDefaults alloc] initWithSuiteName:suiteName]; + [newUserDefaults setObject:@"blabla" forKey:@"fine"]; + XCTAssertEqualObjects([newUserDefaults objectForKey:@"fine"], @"blabla"); + + [newUserDefaults removeObjectForKey:@"fine"]; + XCTAssertNil([newUserDefaults objectForKey:@"fine"]); + [newUserDefaults clearAllData]; + + [self removePreferenceFileWithSuiteName:suiteName]; +} + +- (void)testNewUserDefaultsWithNSUserDefaultsFile { + NSString *suiteName = @"test_suite_file"; + + // Create a user defaults with a key and value. This is to make sure that the new user defaults + // also uses the same plist file. + NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName]; + XCTAssertNil([userDefaults objectForKey:@"key1"]); + XCTAssertNil([userDefaults objectForKey:@"key2"]); + [userDefaults setObject:@"value1" forKey:@"key1"]; + [userDefaults setObject:@"value2" forKey:@"key2"]; + [userDefaults synchronize]; + userDefaults = nil; + + // Now the new user defaults should access the same values. + GULUserDefaults *newUserDefaults = [[GULUserDefaults alloc] initWithSuiteName:suiteName]; + XCTAssertEqualObjects([newUserDefaults objectForKey:@"key1"], @"value1"); + XCTAssertEqualObjects([newUserDefaults objectForKey:@"key2"], @"value2"); + [newUserDefaults clearAllData]; + + // Clean up. + [self removePreferenceFileWithSuiteName:suiteName]; +} + +#pragma mark - Thread-safety test + +- (void)testNewUserDefaultsThreadSafeAddingObjects { + NSString *suiteName = @"test_adding_threadsafe"; + int itemCount = 100; + int itemsPerThread = 10; + GULUserDefaults *userDefaults = [[GULUserDefaults alloc] initWithSuiteName:@"testing"]; + GULMutableDictionary *dictionary = [[GULMutableDictionary alloc] init]; + + // Have 100 threads to add 100 unique keys and values into the dictionary. + for (int threadNum = 0; threadNum < 10; threadNum++) { + GULUserDefaultsThreadArgs *args = [[GULUserDefaultsThreadArgs alloc] init]; + args.userDefaults = userDefaults; + args.dictionary = dictionary; + args.itemsPerThread = itemsPerThread; + args.index = threadNum; + [NSThread detachNewThreadSelector:@selector(addObjectsThread:) toTarget:self withObject:args]; + } + + // Verify the size of the dictionary. + NSPredicate *dictionarySize = [NSPredicate predicateWithFormat:@"count == %d", itemCount]; + XCTestExpectation *expectation = [self expectationForPredicate:dictionarySize + evaluatedWithObject:dictionary + handler:nil]; + [self waitForExpectations:@[expectation] timeout:kGULTestCaseTimeoutInterval]; + + for (int i = 0; i < itemCount; i++) { + NSString *key = [NSString stringWithFormat:@"%d", i]; + XCTAssertEqualObjects([userDefaults objectForKey:key], @(i)); + } + + [userDefaults clearAllData]; + [self removePreferenceFileWithSuiteName:suiteName]; +} + +- (void)testNewUserDefaultsRemovingObjects { + NSString *suiteName = @"test_removing_threadsafe"; + int itemCount = 100; + GULUserDefaults *userDefaults = [[GULUserDefaults alloc] initWithSuiteName:@"testing"]; + GULMutableDictionary *dictionary = [[GULMutableDictionary alloc] init]; + + // Create a dictionary of 100 unique keys and values. + for (int i = 0; i < itemCount; i++) { + NSString *key = [NSString stringWithFormat:@"%d", i]; + [userDefaults setObject:@(i) forKey:key]; + dictionary[key] = @(i); + } + + XCTAssertEqual(dictionary.count, 100); + + // Spawn 10 threads to remove all items inside the dictionary. + int itemsPerThread = 100; + for (int threadNum = 0; threadNum < 10; threadNum++) { + GULUserDefaultsThreadArgs *args = [[GULUserDefaultsThreadArgs alloc] init]; + args.userDefaults = userDefaults; + args.dictionary = dictionary; + args.itemsPerThread = itemsPerThread; + args.index = threadNum; + [NSThread detachNewThreadSelector:@selector(removeObjectsThread:) + toTarget:self + withObject:args]; + } + + // Ensure the dictionary is empty after removing objects. + NSPredicate *emptyDictionary = [NSPredicate predicateWithFormat:@"count == 0"]; + XCTestExpectation *expectation = [self expectationForPredicate:emptyDictionary + evaluatedWithObject:dictionary + handler:nil]; + [self waitForExpectations:@[expectation] timeout:kGULTestCaseTimeoutInterval]; + + for (int i = 0; i < itemCount; i++) { + NSString *key = [NSString stringWithFormat:@"%d", i]; + XCTAssertNil([userDefaults objectForKey:key]); + } + + [userDefaults clearAllData]; + [self removePreferenceFileWithSuiteName:suiteName]; +} + +- (void)testNewUserDefaultsRemovingSomeObjects { + NSString *suiteName = @"test_remove_some_objs"; + int itemCount = 200; + GULUserDefaults *userDefaults = [[GULUserDefaults alloc] initWithSuiteName:suiteName]; + GULMutableDictionary *dictionary = [[GULMutableDictionary alloc] init]; + + // Create a dictionary of 100 unique keys and values. + for (int i = 0; i < itemCount; i++) { + NSString *key = [NSString stringWithFormat:@"%d", i]; + [userDefaults setObject:@(i) forKey:key]; + dictionary[key] = @(i); + } + + // Spawn 10 threads to remove the first 100 items inside the dictionary. + int itemsPerThread = 10; + for (int threadNum = 0; threadNum < 10; threadNum++) { + GULUserDefaultsThreadArgs *args = [[GULUserDefaultsThreadArgs alloc] init]; + args.userDefaults = userDefaults; + args.dictionary = dictionary; + args.itemsPerThread = itemsPerThread; + args.index = threadNum; + [NSThread detachNewThreadSelector:@selector(removeObjectsThread:) + toTarget:self + withObject:args]; + } + + NSPredicate *dictionarySize = [NSPredicate predicateWithFormat:@"count == 100"]; + XCTestExpectation *expectation = [self expectationForPredicate:dictionarySize + evaluatedWithObject:dictionary + handler:nil]; + [self waitForExpectations:@[expectation] timeout:kGULTestCaseTimeoutInterval]; + + // Check the remaining of the user defaults. + for (int i = 0; i < itemCount; i++) { + NSString *key = [NSString stringWithFormat:@"%d", i]; + if (i < 100) { + XCTAssertNil([userDefaults objectForKey:key]); + } else { + XCTAssertEqualObjects([userDefaults objectForKey:key], @(i)); + } + } + [userDefaults clearAllData]; + + [self removePreferenceFileWithSuiteName:suiteName]; +} + +- (void)testBothUserDefaultsThreadSafeAddingObjects { + NSString *suiteName = @"test_adding_both_user_defaults_threadsafe"; + int itemCount = 100; + int itemsPerThread = 10; + GULUserDefaults *newUserDefaults = [[GULUserDefaults alloc] initWithSuiteName:@"testing"]; + NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"testing"]; + GULMutableDictionary *dictionary = [[GULMutableDictionary alloc] init]; + + // Have 100 threads to add 100 unique keys and values into the dictionary. + for (int threadNum = 0; threadNum < 10; threadNum++) { + GULUserDefaultsThreadArgs *args = [[GULUserDefaultsThreadArgs alloc] init]; + args.userDefaults = newUserDefaults; + args.oldUserDefaults = userDefaults; + args.dictionary = dictionary; + args.itemsPerThread = itemsPerThread; + args.index = threadNum; + [NSThread detachNewThreadSelector:@selector(addObjectsBothUserDefaultsThread:) + toTarget:self + withObject:args]; + } + + // Verify the size of the dictionary. + NSPredicate *dictionarySize = [NSPredicate predicateWithFormat:@"count == %d", itemCount]; + XCTestExpectation *expectation = [self expectationForPredicate:dictionarySize + evaluatedWithObject:dictionary + handler:nil]; + [self waitForExpectations:@[expectation] timeout:kGULTestCaseTimeoutInterval]; + + for (int i = 0; i < itemCount; i++) { + NSString *key = [NSString stringWithFormat:@"%d", i]; + if (i % 2 == 0) { + XCTAssertEqualObjects([newUserDefaults objectForKey:key], @(i)); + } else { + XCTAssertEqualObjects([userDefaults objectForKey:key], @(i)); + } + } + + [newUserDefaults clearAllData]; + [self removePreferenceFileWithSuiteName:suiteName]; +} + +- (void)testBothUserDefaultsRemovingSomeObjects { + NSString *suiteName = @"test_remove_some_objs_both_user_defaults"; + int itemCount = 200; + GULUserDefaults *userDefaults = [[GULUserDefaults alloc] initWithSuiteName:suiteName]; + NSUserDefaults *oldUserDefaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName]; + GULMutableDictionary *dictionary = [[GULMutableDictionary alloc] init]; + + // Create a dictionary of 100 unique keys and values. + for (int i = 0; i < itemCount; i++) { + NSString *key = [NSString stringWithFormat:@"%d", i]; + [userDefaults setObject:@(i) forKey:key]; + dictionary[key] = @(i); + } + + // Spawn 10 threads to remove the first 100 items inside the dictionary. + int itemsPerThread = 10; + for (int threadNum = 0; threadNum < 10; threadNum++) { + GULUserDefaultsThreadArgs *args = [[GULUserDefaultsThreadArgs alloc] init]; + args.userDefaults = userDefaults; + args.oldUserDefaults = oldUserDefaults; + args.dictionary = dictionary; + args.itemsPerThread = itemsPerThread; + args.index = threadNum; + [NSThread detachNewThreadSelector:@selector(removeObjectsThread:) + toTarget:self + withObject:args]; + } + + // Verify the size of the dictionary. + NSPredicate *dictionarySize = [NSPredicate predicateWithFormat:@"count == 100"]; + XCTestExpectation *expectation = [self expectationForPredicate:dictionarySize + evaluatedWithObject:dictionary + handler:nil]; + [self waitForExpectations:@[expectation] timeout:kGULTestCaseTimeoutInterval]; + + // Check the remaining of the user defaults. + for (int i = 0; i < itemCount; i++) { + NSString *key = [NSString stringWithFormat:@"%d", i]; + if (i < 100) { + if (i % 2 == 0) { + XCTAssertNil([userDefaults objectForKey:key]); + } else { + XCTAssertNil([oldUserDefaults objectForKey:key]); + } + + } else { + if (i % 2 == 0) { + XCTAssertEqualObjects([userDefaults objectForKey:key], @(i)); + } else { + XCTAssertEqualObjects([oldUserDefaults objectForKey:key], @(i)); + } + } + } + [userDefaults clearAllData]; + + [self removePreferenceFileWithSuiteName:suiteName]; +} + +#pragma mark - Thread methods + +/// Add objects into the current GULUserDefaults given arguments. +- (void)addObjectsThread:(GULUserDefaultsThreadArgs *)args { + int totalItemsPerThread = args.itemsPerThread + args.itemsPerThread * args.index; + for (int i = args.index * args.itemsPerThread; i < totalItemsPerThread; i++) { + NSString *key = [NSString stringWithFormat:@"%d", i]; + [args.userDefaults setObject:@(i) forKey:key]; + args.dictionary[key] = @(i); + } +} + +/// Remove objects from the current GULUserDefaults given arguments. +- (void)removeObjectsThread:(GULUserDefaultsThreadArgs *)args { + int totalItemsPerThread = args.itemsPerThread + args.itemsPerThread * args.index; + for (int i = args.index * args.itemsPerThread; i < totalItemsPerThread; i++) { + NSString *key = [NSString stringWithFormat:@"%d", i]; + [args.userDefaults removeObjectForKey:key]; + [args.dictionary removeObjectForKey:key]; + } +} + +/// Add objects into both user defaults given arguments. +- (void)addObjectsBothUserDefaultsThread:(GULUserDefaultsThreadArgs *)args { + int totalItemsPerThread = args.itemsPerThread + args.itemsPerThread * args.index; + for (int i = args.index * args.itemsPerThread; i < totalItemsPerThread; i++) { + NSString *key = [NSString stringWithFormat:@"%d", i]; + if (i % 2 == 0) { + [args.userDefaults setObject:@(i) forKey:key]; + } else { + [args.oldUserDefaults setObject:@(i) forKey:key]; + } + args.dictionary[key] = @(i); + } +} + +/// Remove objects from both user defaults given arguments. +- (void)removeObjectsFromBothUserDefaultsThread:(GULUserDefaultsThreadArgs *)args { + int totalItemsPerThread = args.itemsPerThread + args.itemsPerThread * args.index; + for (int i = args.index * args.itemsPerThread; i < totalItemsPerThread; i++) { + NSString *key = [NSString stringWithFormat:@"%d", i]; + if (i % 2 == 0) { + [args.userDefaults removeObjectForKey:key]; + } else { + [args.oldUserDefaults removeObjectForKey:key]; + } + + [args.dictionary removeObjectForKey:key]; + } +} + +#pragma mark - Helper + +- (NSString *)filePathForPreferencesName:(NSString *)preferencesName { + if (!preferencesName.length) { + return @""; + } + + // User Defaults exist in the Library directory, get the path to use it as a prefix. + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); + if (!paths.lastObject) { + XCTFail(@"Library directory not found - NSSearchPath results are empty."); + } + NSArray *components = @[ + paths.lastObject, + @"Preferences", + [preferencesName stringByAppendingPathExtension:@"plist"] + ]; + return [NSString pathWithComponents:components]; +} + +- (void)removePreferenceFileWithSuiteName:(NSString *)suiteName { + NSString *path = [self filePathForPreferencesName:suiteName]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + if ([fileManager fileExistsAtPath:path]) { + XCTAssertTrue([fileManager removeItemAtPath:path error:NULL]); + } +} + +@end diff --git a/GoogleUtilities/UserDefaults/GULUserDefaults.m b/GoogleUtilities/UserDefaults/GULUserDefaults.m new file mode 100644 index 00000000000..1b1bafb76e1 --- /dev/null +++ b/GoogleUtilities/UserDefaults/GULUserDefaults.m @@ -0,0 +1,235 @@ +// Copyright 2018 Google +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "Private/GULUserDefaults.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +static NSTimeInterval const kGULSynchronizeInterval = 1.0; + +static NSString *const kGULLogFormat = @"I-GUL%06ld"; + +static GULLoggerService kGULLogUserDefaultsService = @"[GoogleUtilities/UserDefaults]"; + +typedef NS_ENUM(NSInteger, GULUDMessageCode) { + GULUDMessageCodeInvalidKeyGet = 1, + GULUDMessageCodeInvalidKeySet = 2, + GULUDMessageCodeInvalidObjectSet = 3, + GULUDMessageCodeSynchronizeFailed = 4, +}; + +@interface GULUserDefaults () + +/// Equivalent to the suite name for NSUserDefaults. +@property(readonly) CFStringRef appNameRef; + +@property(atomic) BOOL isPreferenceFileExcluded; + +@end + +@implementation GULUserDefaults { + // The application name is the same with the suite name of the NSUserDefaults, and it is used for + // preferences. + CFStringRef _appNameRef; +} + ++ (GULUserDefaults *)standardUserDefaults { + static GULUserDefaults *standardUserDefaults; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + standardUserDefaults = [[GULUserDefaults alloc] init]; + }); + return standardUserDefaults; +} + +- (instancetype)init { + return [self initWithSuiteName:nil]; +} + +- (instancetype)initWithSuiteName:(nullable NSString *)suiteName { + self = [super init]; + + NSString *name = [suiteName copy]; + + if (self) { + // `kCFPreferencesCurrentApplication` maps to the same defaults database as + // `[NSUserDefaults standardUserDefaults]`. + _appNameRef = + name.length ? (__bridge_retained CFStringRef)name : kCFPreferencesCurrentApplication; + } + + return self; +} + +- (void)dealloc { + // If we're using a custom `_appNameRef` it needs to be released. If it's a constant, it shouldn't + // need to be released since we don't own it. + if (CFStringCompare(_appNameRef, kCFPreferencesCurrentApplication, 0) != kCFCompareEqualTo) { + CFRelease(_appNameRef); + } + + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(synchronize) + object:nil]; +} + +- (nullable id)objectForKey:(NSString *)defaultName { + NSString *key = [defaultName copy]; + if (![key isKindOfClass:[NSString class]] || !key.length) { + GULLogWarning(@"", NO, + [NSString stringWithFormat:kGULLogFormat, (long)GULUDMessageCodeInvalidKeyGet], + @"Cannot get object for invalid user default key."); + return nil; + } + return (__bridge_transfer id)CFPreferencesCopyAppValue((__bridge CFStringRef)key, _appNameRef); +} + +- (void)setObject:(nullable id)value forKey:(NSString *)defaultName { + NSString *key = [defaultName copy]; + if (![key isKindOfClass:[NSString class]] || !key.length) { + GULLogWarning(kGULLogUserDefaultsService, NO, + [NSString stringWithFormat:kGULLogFormat, (long)GULUDMessageCodeInvalidKeySet], + @"Cannot set object for invalid user default key."); + return; + } + if (!value) { + CFPreferencesSetAppValue((__bridge CFStringRef)key, NULL, _appNameRef); + [self scheduleSynchronize]; + return; + } + BOOL isAcceptableValue = + [value isKindOfClass:[NSString class]] || [value isKindOfClass:[NSNumber class]] || + [value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSDictionary class]] || + [value isKindOfClass:[NSDate class]] || [value isKindOfClass:[NSData class]]; + if (!isAcceptableValue) { + GULLogWarning(kGULLogUserDefaultsService, NO, + [NSString stringWithFormat:kGULLogFormat, (long)GULUDMessageCodeInvalidObjectSet], + @"Cannot set invalid object to user defaults. Must be a string, number, array, " + @"dictionary, date, or data. Value: %@", + value); + return; + } + + CFPreferencesSetAppValue((__bridge CFStringRef)key, (__bridge CFStringRef)value, _appNameRef); + [self scheduleSynchronize]; +} + +- (void)removeObjectForKey:(NSString *)key { + [self setObject:nil forKey:key]; +} + +#pragma mark - Getters + +- (NSInteger)integerForKey:(NSString *)defaultName { + NSNumber *object = [self objectForKey:defaultName]; + return object.integerValue; +} + +- (float)floatForKey:(NSString *)defaultName { + NSNumber *object = [self objectForKey:defaultName]; + return object.floatValue; +} + +- (double)doubleForKey:(NSString *)defaultName { + NSNumber *object = [self objectForKey:defaultName]; + return object.doubleValue; +} + +- (BOOL)boolForKey:(NSString *)defaultName { + NSNumber *object = [self objectForKey:defaultName]; + return object.boolValue; +} + +- (nullable NSString *)stringForKey:(NSString *)defaultName { + return [self objectForKey:defaultName]; +} + +- (nullable NSArray *)arrayForKey:(NSString *)defaultName { + return [self objectForKey:defaultName]; +} + +- (nullable NSDictionary *)dictionaryForKey:(NSString *)defaultName { + return [self objectForKey:defaultName]; +} + +#pragma mark - Setters + +- (void)setInteger:(NSInteger)integer forKey:(NSString *)defaultName { + [self setObject:@(integer) forKey:defaultName]; +} + +- (void)setFloat:(float)value forKey:(NSString *)defaultName { + [self setObject:@(value) forKey:defaultName]; +} + +- (void)setDouble:(double)doubleNumber forKey:(NSString *)defaultName { + [self setObject:@(doubleNumber) forKey:defaultName]; +} + +- (void)setBool:(BOOL)boolValue forKey:(NSString *)defaultName { + [self setObject:@(boolValue) forKey:defaultName]; +} + +#pragma mark - Save data + +- (void)synchronize { + if (!CFPreferencesAppSynchronize(_appNameRef)) { + GULLogError(kGULLogUserDefaultsService, NO, + [NSString stringWithFormat:kGULLogFormat, (long)GULUDMessageCodeSynchronizeFailed], + @"Cannot synchronize user defaults to disk"); + } +} + +#pragma mark - Private methods + +/// Removes all values from the search list entry specified by 'domainName', the current user, and +/// any host. The change is persistent. Equivalent to -removePersistentDomainForName: of +/// NSUserDefaults. +- (void)clearAllData { + // On macOS, using `kCFPreferencesCurrentHost` will not set all the keys necessary to match + // `NSUserDefaults`. +#if TARGET_OS_MAC + CFStringRef host = kCFPreferencesAnyHost; +#else + CFStringRef host = kCFPreferencesCurrentHost; +#endif // TARGET_OS_OSX + + CFArrayRef keyList = CFPreferencesCopyKeyList(_appNameRef, kCFPreferencesCurrentUser, host); + if (!keyList) { + return; + } + + CFPreferencesSetMultiple(NULL, keyList, _appNameRef, kCFPreferencesCurrentUser, host); + CFRelease(keyList); + [self scheduleSynchronize]; +} + +- (void)scheduleSynchronize { + // Synchronize data using a timer so that multiple set... calls can be coalesced under one + // synchronize. + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(synchronize) + object:nil]; + // This method may be called on multiple queues (due to set... methods can be called on any queue) + // synchronize can be scheduled on different queues, so make sure that it does not crash. If this + // instance goes away, self will be released also, no one will retain it and the schedule won't be + // called. + [self performSelector:@selector(synchronize) withObject:nil afterDelay:kGULSynchronizeInterval]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleUtilities/UserDefaults/Private/GULUserDefaults.h b/GoogleUtilities/UserDefaults/Private/GULUserDefaults.h new file mode 100644 index 00000000000..0d04781841d --- /dev/null +++ b/GoogleUtilities/UserDefaults/Private/GULUserDefaults.h @@ -0,0 +1,110 @@ +// Copyright 2018 Google +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// A thread-safe user defaults that uses C functions from CFPreferences.h instead of +/// `NSUserDefaults`. This is to avoid sending an `NSNotification` when it's changed from a +/// background thread to avoid crashing. // TODO: Insert radar number here. +@interface GULUserDefaults : NSObject + +/// A shared user defaults similar to +[NSUserDefaults standardUserDefaults] and accesses the same +/// data of the standardUserDefaults. ++ (GULUserDefaults *)standardUserDefaults; + +/// Initializes preferences with a suite name that is the same with the NSUserDefaults' suite name. +/// Both of CFPreferences and NSUserDefaults share the same plist file so their data will exactly +/// the same. +/// +/// @param suiteName The name of the suite of the user defaults. +- (instancetype)initWithSuiteName:(nullable NSString *)suiteName; + +#pragma mark - Getters + +/// Searches the receiver's search list for a default with the key 'defaultName' and return it. If +/// another process has changed defaults in the search list, NSUserDefaults will automatically +/// update to the latest values. If the key in question has been marked as ubiquitous via a Defaults +/// Configuration File, the latest value may not be immediately available, and the registered value +/// will be returned instead. +- (nullable id)objectForKey:(NSString *)defaultName; + +/// Equivalent to -objectForKey:, except that it will return nil if the value is not an NSArray. +- (nullable NSArray *)arrayForKey:(NSString *)defaultName; + +/// Equivalent to -objectForKey:, except that it will return nil if the value +/// is not an NSDictionary. +- (nullable NSDictionary *)dictionaryForKey:(NSString *)defaultName; + +/// Equivalent to -objectForKey:, except that it will convert NSNumber values to their NSString +/// representation. If a non-string non-number value is found, nil will be returned. +- (nullable NSString *)stringForKey:(NSString *)defaultName; + +/// Equivalent to -objectForKey:, except that it converts the returned value to an NSInteger. If the +/// value is an NSNumber, the result of -integerValue will be returned. If the value is an NSString, +/// it will be converted to NSInteger if possible. If the value is a boolean, it will be converted +/// to either 1 for YES or 0 for NO. If the value is absent or can't be converted to an integer, 0 +/// will be returned. +- (NSInteger)integerForKey:(NSString *)defaultName; + +/// Similar to -integerForKey:, except that it returns a float, and boolean values will not be +/// converted. +- (float)floatForKey:(NSString *)defaultName; + +/// Similar to -integerForKey:, except that it returns a double, and boolean values will not be +/// converted. +- (double)doubleForKey:(NSString *)defaultName; + +/// Equivalent to -objectForKey:, except that it converts the returned value to a BOOL. If the value +/// is an NSNumber, NO will be returned if the value is 0, YES otherwise. If the value is an +/// NSString, values of "YES" or "1" will return YES, and values of "NO", "0", or any other string +/// will return NO. If the value is absent or can't be converted to a BOOL, NO will be returned. +- (BOOL)boolForKey:(NSString *)defaultName; + +#pragma mark - Setters + +/// Immediately stores a value (or removes the value if `nil` is passed as the value) for the +/// provided key in the search list entry for the receiver's suite name in the current user and any +/// host, then asynchronously stores the value persistently, where it is made available to other +/// processes. +- (void)setObject:(nullable id)value forKey:(NSString *)defaultName; + +/// Equivalent to -setObject:forKey: except that the value is converted from a float to an NSNumber. +- (void)setFloat:(float)value forKey:(NSString *)defaultName; + +/// Equivalent to -setObject:forKey: except that the value is converted from a double to an +/// NSNumber. +- (void)setDouble:(double)value forKey:(NSString *)defaultName; + +/// Equivalent to -setObject:forKey: except that the value is converted from an NSInteger to an +/// NSNumber. +- (void)setInteger:(NSInteger)value forKey:(NSString *)defaultName; + +/// Equivalent to -setObject:forKey: except that the value is converted from a BOOL to an NSNumber. +- (void)setBool:(BOOL)value forKey:(NSString *)defaultName; + +#pragma mark - Removing Defaults + +/// Equivalent to -[... setObject:nil forKey:defaultName] +- (void)removeObjectForKey:(NSString *)defaultName; + +#pragma mark - Save data + +/// Blocks the calling thread until all in-progress set operations have completed. +- (void)synchronize; + +@end + +NS_ASSUME_NONNULL_END