blob: a91690c0dbc09bef1a311777e5bbb326264169d3 [file] [log] [blame]
/* Copyright 2015 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. */
/**
* @fileoverview
* 'settings-prefs' exposes a singleton model of Chrome settings and
* preferences, which listens to changes to Chrome prefs whitelisted in
* chrome.settingsPrivate. When changing prefs in this element's 'prefs'
* property via the UI, the singleton model tries to set those preferences in
* Chrome. Whether or not the calls to settingsPrivate.setPref succeed, 'prefs'
* is eventually consistent with the Chrome pref store.
*/
(function() {
'use strict';
/**
* Checks whether two values are recursively equal. Only compares serializable
* data (primitives, serializable arrays and serializable objects).
* @param {*} val1 Value to compare.
* @param {*} val2 Value to compare with val1.
* @return {boolean} True if the values are recursively equal.
*/
function deepEqual(val1, val2) {
if (val1 === val2)
return true;
if (Array.isArray(val1) || Array.isArray(val2)) {
if (!Array.isArray(val1) || !Array.isArray(val2))
return false;
return arraysEqual(
/** @type {!Array} */ (val1),
/** @type {!Array} */ (val2));
}
if (val1 instanceof Object && val2 instanceof Object)
return objectsEqual(val1, val2);
return false;
}
/**
* @param {!Array} arr1
* @param {!Array} arr2
* @return {boolean} True if the arrays are recursively equal.
*/
function arraysEqual(arr1, arr2) {
if (arr1.length != arr2.length)
return false;
for (let i = 0; i < arr1.length; i++) {
if (!deepEqual(arr1[i], arr2[i]))
return false;
}
return true;
}
/**
* @param {!Object} obj1
* @param {!Object} obj2
* @return {boolean} True if the objects are recursively equal.
*/
function objectsEqual(obj1, obj2) {
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length != keys2.length)
return false;
for (let i = 0; i < keys1.length; i++) {
const key = keys1[i];
if (!deepEqual(obj1[key], obj2[key]))
return false;
}
return true;
}
/**
* Returns a recursive copy of the value.
* @param {*} val Value to copy. Should be a primitive or only contain
* serializable data (primitives, serializable arrays and
* serializable objects).
* @return {*} A deep copy of the value.
*/
function deepCopy(val) {
if (!(val instanceof Object))
return val;
return Array.isArray(val) ? deepCopyArray(/** @type {!Array} */ (val)) :
deepCopyObject(val);
}
/**
* @param {!Array} arr
* @return {!Array} Deep copy of the array.
*/
function deepCopyArray(arr) {
const copy = [];
for (let i = 0; i < arr.length; i++)
copy.push(deepCopy(arr[i]));
return copy;
}
/**
* @param {!Object} obj
* @return {!Object} Deep copy of the object.
*/
function deepCopyObject(obj) {
const copy = {};
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
copy[key] = deepCopy(obj[key]);
}
return copy;
}
Polymer({
is: 'settings-prefs',
properties: {
/**
* Object containing all preferences, for use by Polymer controls.
* @type {Object|undefined}
*/
prefs: {
type: Object,
notify: true,
},
/**
* Map of pref keys to values representing the state of the Chrome
* pref store as of the last update from the API.
* @type {Object<*>}
* @private
*/
lastPrefValues_: {
type: Object,
value: function() {
return {};
},
},
},
observers: [
'prefsChanged_(prefs.*)',
],
/** @type {SettingsPrivate} */
settingsApi_: /** @type {SettingsPrivate} */ (chrome.settingsPrivate),
/** @override */
created: function() {
if (!CrSettingsPrefs.deferInitialization)
this.initialize();
},
/** @override */
detached: function() {
CrSettingsPrefs.resetForTesting();
},
/**
* @param {SettingsPrivate=} opt_settingsApi SettingsPrivate implementation
* to use (chrome.settingsPrivate by default).
*/
initialize: function(opt_settingsApi) {
// Only initialize once (or after resetForTesting() is called).
if (this.initialized_)
return;
this.initialized_ = true;
if (opt_settingsApi)
this.settingsApi_ = opt_settingsApi;
/** @private {function(!Array<!chrome.settingsPrivate.PrefObject>)} */
this.boundPrefsChanged_ = this.onSettingsPrivatePrefsChanged_.bind(this);
this.settingsApi_.onPrefsChanged.addListener(this.boundPrefsChanged_);
this.settingsApi_.getAllPrefs(
this.onSettingsPrivatePrefsFetched_.bind(this));
},
/**
* @param {!{path: string}} e
* @private
*/
prefsChanged_: function(e) {
// |prefs| can be directly set or unset in tests.
if (!CrSettingsPrefs.isInitialized || e.path == 'prefs')
return;
const key = this.getPrefKeyFromPath_(e.path);
const prefStoreValue = this.lastPrefValues_[key];
const prefObj = /** @type {chrome.settingsPrivate.PrefObject} */ (
this.get(key, this.prefs));
// If settingsPrivate already has this value, ignore it. (Otherwise,
// a change event from settingsPrivate could make us call
// settingsPrivate.setPref and potentially trigger an IPC loop.)
if (!deepEqual(prefStoreValue, prefObj.value)) {
this.settingsApi_.setPref(
key, prefObj.value,
/* pageId */ '',
/* callback */ this.setPrefCallback_.bind(this, key));
}
},
/**
* Called when prefs in the underlying Chrome pref store are changed.
* @param {!Array<!chrome.settingsPrivate.PrefObject>} prefs
* The prefs that changed.
* @private
*/
onSettingsPrivatePrefsChanged_: function(prefs) {
if (CrSettingsPrefs.isInitialized)
this.updatePrefs_(prefs);
},
/**
* Called when prefs are fetched from settingsPrivate.
* @param {!Array<!chrome.settingsPrivate.PrefObject>} prefs
* @private
*/
onSettingsPrivatePrefsFetched_: function(prefs) {
this.updatePrefs_(prefs);
CrSettingsPrefs.setInitialized();
},
/**
* Checks the result of calling settingsPrivate.setPref.
* @param {string} key The key used in the call to setPref.
* @param {boolean} success True if setting the pref succeeded.
* @private
*/
setPrefCallback_: function(key, success) {
if (!success)
this.refresh(key);
},
/**
* Get the current pref value from chrome.settingsPrivate to ensure the UI
* stays up to date.
* @param {string} key
*/
refresh: function(key) {
this.settingsApi_.getPref(key, pref => {
this.updatePrefs_([pref]);
});
},
/**
* Updates the prefs model with the given prefs.
* @param {!Array<!chrome.settingsPrivate.PrefObject>} newPrefs
* @private
*/
updatePrefs_: function(newPrefs) {
// Use the existing prefs object or create it.
const prefs = this.prefs || {};
newPrefs.forEach(function(newPrefObj) {
// Use the PrefObject from settingsPrivate to create a copy in
// lastPrefValues_ at the pref's key.
this.lastPrefValues_[newPrefObj.key] = deepCopy(newPrefObj.value);
if (!deepEqual(this.get(newPrefObj.key, prefs), newPrefObj)) {
// Add the pref to |prefs|.
cr.exportPath(newPrefObj.key, newPrefObj, prefs);
// If this.prefs already exists, notify listeners of the change.
if (prefs == this.prefs)
this.notifyPath('prefs.' + newPrefObj.key, newPrefObj);
}
}, this);
if (!this.prefs)
this.prefs = prefs;
},
/**
* Given a 'property-changed' path, returns the key of the preference the
* path refers to. E.g., if the path of the changed property is
* 'prefs.search.suggest_enabled.value', the key of the pref that changed is
* 'search.suggest_enabled'.
* @param {string} path
* @return {string}
* @private
*/
getPrefKeyFromPath_: function(path) {
// Skip the first token, which refers to the member variable (this.prefs).
const parts = path.split('.');
assert(parts.shift() == 'prefs', 'Path doesn\'t begin with \'prefs\'');
for (let i = 1; i <= parts.length; i++) {
const key = parts.slice(0, i).join('.');
// The lastPrefValues_ keys match the pref keys.
if (this.lastPrefValues_.hasOwnProperty(key))
return key;
}
return '';
},
/**
* Resets the element so it can be re-initialized with a new prefs state.
*/
resetForTesting: function() {
if (!this.initialized_)
return;
this.prefs = undefined;
this.lastPrefValues_ = {};
this.initialized_ = false;
// Remove the listener added in initialize().
this.settingsApi_.onPrefsChanged.removeListener(this.boundPrefsChanged_);
this.settingsApi_ =
/** @type {SettingsPrivate} */ (chrome.settingsPrivate);
},
});
})();