blob: 5b0876c8627c694f8dae2711da9f9602449334d8 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ios/chrome/app/startup/register_experimental_settings.h"
#include "base/logging.h"
#include "base/mac/bundle_locations.h"
#include "base/strings/sys_string_conversions.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Key in the UserDefaults for the Experimental Keys.
NSString* kExperimentalKeysKey = @"ExperimentalKeys";
// Returns YES if a setting value is equivalent to not having the setting at
// all. This must always be true for default values, otherwise the experimental
// settings will have different default behaviors in stable channel (where the
// bundle isn't present).
BOOL IsDefaultSettingValueValid(id value) {
if (!value)
return YES;
if ([value isKindOfClass:[NSNumber class]])
return [value intValue] == 0;
if ([value isKindOfClass:[NSString class]])
return [value length] == 0;
// Add support for other types as necessary.
NOTREACHED() << "Unhandled value type "
<< base::SysNSStringToUTF8(NSStringFromClass([value class]));
return NO;
}
} // namespace
@interface RegisterExperimentalSettings ()
// Registers all the default values for a single settings file and returns
// all the registered keys.
+ (NSArray*)registerExperimentalSettingsForFile:(NSString*)filepath
userDefaults:(NSUserDefaults*)userDefaults;
@end
@implementation RegisterExperimentalSettings
+ (void)registerExperimentalSettingsWithUserDefaults:
(NSUserDefaults*)userDefaults
bundle:(NSBundle*)bundle {
// Save the current app version in user defaults.
NSDictionary* infoDictionary = [bundle infoDictionary];
NSString* version = [infoDictionary objectForKey:@"CFBundleVersion"];
[userDefaults setObject:version forKey:@"Version"];
NSString* bundlePath = [bundle bundlePath];
NSString* settingsFilepath =
[bundlePath stringByAppendingPathComponent:@"Settings.bundle"];
NSArray* settingsContent =
[[NSFileManager defaultManager] contentsOfDirectoryAtPath:settingsFilepath
error:NULL];
NSMutableArray* currentExpKeys = [[NSMutableArray alloc] init];
for (NSString* filename in settingsContent) {
// Only plist files are preferences definition.
if ([[filename pathExtension] isEqualToString:@"plist"]) {
NSString* filepath =
[settingsFilepath stringByAppendingPathComponent:filename];
NSArray* registeredKeys =
[self registerExperimentalSettingsForFile:filepath
userDefaults:userDefaults];
[currentExpKeys addObjectsFromArray:registeredKeys];
}
}
// Remove all keys that are no longer used.
NSArray* expKeys = [userDefaults arrayForKey:kExperimentalKeysKey];
NSMutableSet* expKeysSet = [NSMutableSet setWithArray:expKeys];
NSSet* currentExpKeysSet = [NSSet setWithArray:currentExpKeys];
[expKeysSet minusSet:currentExpKeysSet];
for (NSString* key in expKeysSet) {
[userDefaults removeObjectForKey:key];
}
if ([currentExpKeys count] > 0) {
[userDefaults setObject:currentExpKeys forKey:kExperimentalKeysKey];
} else {
[userDefaults removeObjectForKey:kExperimentalKeysKey];
}
}
+ (NSArray*)registerExperimentalSettingsForFile:(NSString*)filepath
userDefaults:(NSUserDefaults*)userDefaults {
NSMutableArray* registeredKeys = [NSMutableArray array];
NSDictionary* rootDictionary =
[NSDictionary dictionaryWithContentsOfFile:filepath];
// Array with all the preference specifiers. The plist is composed of many
// Preference specifiers; one for each preference row in the settings
// panel.
NSArray* preferencesArray =
[rootDictionary objectForKey:@"PreferenceSpecifiers"];
// Scan thru all the preferences in the plist file.
for (NSDictionary* preferenceSpecifier in preferencesArray) {
NSString* keyValue = [preferenceSpecifier objectForKey:@"Key"];
if (!keyValue)
continue;
id defaultValue = [preferenceSpecifier objectForKey:@"DefaultValue"];
// Within the app, the default for all experimental prefs is nil (matching
// the behavior of Stable channel, where there is no settings bundle). To
// make mistakes obvious, fail if someone tries to set any actual value as
// the default.
DCHECK(IsDefaultSettingValueValid(defaultValue))
<< "'" << base::SysNSStringToUTF8([defaultValue description])
<< "' is not a valid default value for "
<< base::SysNSStringToUTF8(keyValue);
[registeredKeys addObject:keyValue];
// If a default value is set, normalize it to nil.
id currentValue = [userDefaults objectForKey:keyValue];
if (currentValue &&
(!defaultValue || [currentValue isEqual:defaultValue])) {
[userDefaults removeObjectForKey:keyValue];
}
}
return registeredKeys;
}
@end