| OLD | NEW |
| 1 /* Copyright 2015 The Chromium Authors. All rights reserved. | 1 /* Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 * Use of this source code is governed by a BSD-style license that can be | 2 * Use of this source code is governed by a BSD-style license that can be |
| 3 * found in the LICENSE file. */ | 3 * found in the LICENSE file. */ |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * @fileoverview | 6 * @fileoverview |
| 7 * 'settings-prefs' exposes a singleton model of Chrome settings and | 7 * 'settings-prefs' exposes a singleton model of Chrome settings and |
| 8 * preferences, which listens to changes to Chrome prefs whitelisted in | 8 * preferences, which listens to changes to Chrome prefs whitelisted in |
| 9 * chrome.settingsPrivate. When changing prefs in this element's 'prefs' | 9 * chrome.settingsPrivate. When changing prefs in this element's 'prefs' |
| 10 * property via the UI, the singleton model tries to set those preferences in | 10 * property via the UI, the singleton model tries to set those preferences in |
| 11 * Chrome. Whether or not the calls to settingsPrivate.setPref succeed, 'prefs' | 11 * Chrome. Whether or not the calls to settingsPrivate.setPref succeed, 'prefs' |
| 12 * is eventually consistent with the Chrome pref store. | 12 * is eventually consistent with the Chrome pref store. |
| 13 */ | 13 */ |
| 14 | 14 |
| 15 (function() { | 15 (function() { |
| 16 'use strict'; | 16 'use strict'; |
| 17 | 17 |
| 18 /** | 18 /** |
| 19 * Checks whether two values are recursively equal. Only compares serializable | 19 * Checks whether two values are recursively equal. Only compares serializable |
| 20 * data (primitives, serializable arrays and serializable objects). | 20 * data (primitives, serializable arrays and serializable objects). |
| 21 * @param {*} val1 Value to compare. | 21 * @param {*} val1 Value to compare. |
| 22 * @param {*} val2 Value to compare with val1. | 22 * @param {*} val2 Value to compare with val1. |
| 23 * @return {boolean} True if the values are recursively equal. | 23 * @return {boolean} True if the values are recursively equal. |
| 24 */ | 24 */ |
| 25 function deepEqual(val1, val2) { | 25 function deepEqual(val1, val2) { |
| 26 if (val1 === val2) | 26 if (val1 === val2) |
| 27 return true; | 27 return true; |
| 28 | 28 |
| 29 if (Array.isArray(val1) || Array.isArray(val2)) { | 29 if (Array.isArray(val1) || Array.isArray(val2)) { |
| 30 if (!Array.isArray(val1) || !Array.isArray(val2)) | 30 if (!Array.isArray(val1) || !Array.isArray(val2)) |
| 31 return false; | 31 return false; |
| 32 return arraysEqual(/** @type {!Array} */(val1), | 32 return arraysEqual( |
| 33 /** @type {!Array} */(val2)); | 33 /** @type {!Array} */ (val1), |
| 34 } | 34 /** @type {!Array} */ (val2)); |
| 35 | 35 } |
| 36 if (val1 instanceof Object && val2 instanceof Object) | 36 |
| 37 return objectsEqual(val1, val2); | 37 if (val1 instanceof Object && val2 instanceof Object) |
| 38 | 38 return objectsEqual(val1, val2); |
| 39 |
| 40 return false; |
| 41 } |
| 42 |
| 43 /** |
| 44 * @param {!Array} arr1 |
| 45 * @param {!Array} arr2 |
| 46 * @return {boolean} True if the arrays are recursively equal. |
| 47 */ |
| 48 function arraysEqual(arr1, arr2) { |
| 49 if (arr1.length != arr2.length) |
| 39 return false; | 50 return false; |
| 51 |
| 52 for (var i = 0; i < arr1.length; i++) { |
| 53 if (!deepEqual(arr1[i], arr2[i])) |
| 54 return false; |
| 40 } | 55 } |
| 41 | 56 |
| 42 /** | 57 return true; |
| 43 * @param {!Array} arr1 | 58 } |
| 44 * @param {!Array} arr2 | 59 |
| 45 * @return {boolean} True if the arrays are recursively equal. | 60 /** |
| 46 */ | 61 * @param {!Object} obj1 |
| 47 function arraysEqual(arr1, arr2) { | 62 * @param {!Object} obj2 |
| 48 if (arr1.length != arr2.length) | 63 * @return {boolean} True if the objects are recursively equal. |
| 64 */ |
| 65 function objectsEqual(obj1, obj2) { |
| 66 var keys1 = Object.keys(obj1); |
| 67 var keys2 = Object.keys(obj2); |
| 68 if (keys1.length != keys2.length) |
| 69 return false; |
| 70 |
| 71 for (var i = 0; i < keys1.length; i++) { |
| 72 var key = keys1[i]; |
| 73 if (!deepEqual(obj1[key], obj2[key])) |
| 49 return false; | 74 return false; |
| 50 | |
| 51 for (var i = 0; i < arr1.length; i++) { | |
| 52 if (!deepEqual(arr1[i], arr2[i])) | |
| 53 return false; | |
| 54 } | |
| 55 | |
| 56 return true; | |
| 57 } | 75 } |
| 58 | 76 |
| 59 /** | 77 return true; |
| 60 * @param {!Object} obj1 | 78 } |
| 61 * @param {!Object} obj2 | 79 |
| 62 * @return {boolean} True if the objects are recursively equal. | 80 /** |
| 63 */ | 81 * Returns a recursive copy of the value. |
| 64 function objectsEqual(obj1, obj2) { | 82 * @param {*} val Value to copy. Should be a primitive or only contain |
| 65 var keys1 = Object.keys(obj1); | 83 * serializable data (primitives, serializable arrays and |
| 66 var keys2 = Object.keys(obj2); | 84 * serializable objects). |
| 67 if (keys1.length != keys2.length) | 85 * @return {*} A deep copy of the value. |
| 68 return false; | 86 */ |
| 69 | 87 function deepCopy(val) { |
| 70 for (var i = 0; i < keys1.length; i++) { | 88 if (!(val instanceof Object)) |
| 71 var key = keys1[i]; | 89 return val; |
| 72 if (!deepEqual(obj1[key], obj2[key])) | 90 return Array.isArray(val) ? deepCopyArray(/** @type {!Array} */ (val)) : |
| 73 return false; | 91 deepCopyObject(val); |
| 74 } | 92 } |
| 75 | 93 |
| 76 return true; | 94 /** |
| 95 * @param {!Array} arr |
| 96 * @return {!Array} Deep copy of the array. |
| 97 */ |
| 98 function deepCopyArray(arr) { |
| 99 var copy = []; |
| 100 for (var i = 0; i < arr.length; i++) |
| 101 copy.push(deepCopy(arr[i])); |
| 102 return copy; |
| 103 } |
| 104 |
| 105 /** |
| 106 * @param {!Object} obj |
| 107 * @return {!Object} Deep copy of the object. |
| 108 */ |
| 109 function deepCopyObject(obj) { |
| 110 var copy = {}; |
| 111 var keys = Object.keys(obj); |
| 112 for (var i = 0; i < keys.length; i++) { |
| 113 var key = keys[i]; |
| 114 copy[key] = deepCopy(obj[key]); |
| 77 } | 115 } |
| 78 | 116 return copy; |
| 79 /** | 117 } |
| 80 * Returns a recursive copy of the value. | 118 |
| 81 * @param {*} val Value to copy. Should be a primitive or only contain | 119 Polymer({ |
| 82 * serializable data (primitives, serializable arrays and | 120 is: 'settings-prefs', |
| 83 * serializable objects). | 121 |
| 84 * @return {*} A deep copy of the value. | 122 properties: { |
| 85 */ | 123 /** |
| 86 function deepCopy(val) { | 124 * Object containing all preferences, for use by Polymer controls. |
| 87 if (!(val instanceof Object)) | 125 * @type {Object|undefined} |
| 88 return val; | 126 */ |
| 89 return Array.isArray(val) ? deepCopyArray(/** @type {!Array} */(val)) : | 127 prefs: { |
| 90 deepCopyObject(val); | 128 type: Object, |
| 91 } | 129 notify: true, |
| 92 | 130 }, |
| 93 /** | 131 |
| 94 * @param {!Array} arr | 132 /** |
| 95 * @return {!Array} Deep copy of the array. | 133 * Map of pref keys to values representing the state of the Chrome |
| 96 */ | 134 * pref store as of the last update from the API. |
| 97 function deepCopyArray(arr) { | 135 * @type {Object<*>} |
| 98 var copy = []; | 136 * @private |
| 99 for (var i = 0; i < arr.length; i++) | 137 */ |
| 100 copy.push(deepCopy(arr[i])); | 138 lastPrefValues_: { |
| 101 return copy; | 139 type: Object, |
| 102 } | 140 value: function() { |
| 103 | 141 return {}; |
| 104 /** | |
| 105 * @param {!Object} obj | |
| 106 * @return {!Object} Deep copy of the object. | |
| 107 */ | |
| 108 function deepCopyObject(obj) { | |
| 109 var copy = {}; | |
| 110 var keys = Object.keys(obj); | |
| 111 for (var i = 0; i < keys.length; i++) { | |
| 112 var key = keys[i]; | |
| 113 copy[key] = deepCopy(obj[key]); | |
| 114 } | |
| 115 return copy; | |
| 116 } | |
| 117 | |
| 118 Polymer({ | |
| 119 is: 'settings-prefs', | |
| 120 | |
| 121 properties: { | |
| 122 /** | |
| 123 * Object containing all preferences, for use by Polymer controls. | |
| 124 * @type {Object|undefined} | |
| 125 */ | |
| 126 prefs: { | |
| 127 type: Object, | |
| 128 notify: true, | |
| 129 }, | |
| 130 | |
| 131 /** | |
| 132 * Map of pref keys to values representing the state of the Chrome | |
| 133 * pref store as of the last update from the API. | |
| 134 * @type {Object<*>} | |
| 135 * @private | |
| 136 */ | |
| 137 lastPrefValues_: { | |
| 138 type: Object, | |
| 139 value: function() { return {}; }, | |
| 140 }, | 142 }, |
| 141 }, | 143 }, |
| 142 | 144 }, |
| 143 observers: [ | 145 |
| 144 'prefsChanged_(prefs.*)', | 146 observers: [ |
| 145 ], | 147 'prefsChanged_(prefs.*)', |
| 146 | 148 ], |
| 147 /** @type {SettingsPrivate} */ | 149 |
| 148 settingsApi_: /** @type {SettingsPrivate} */(chrome.settingsPrivate), | 150 /** @type {SettingsPrivate} */ |
| 149 | 151 settingsApi_: /** @type {SettingsPrivate} */ (chrome.settingsPrivate), |
| 150 /** @override */ | 152 |
| 151 created: function() { | 153 /** @override */ |
| 152 if (!CrSettingsPrefs.deferInitialization) | 154 created: function() { |
| 153 this.initialize(); | 155 if (!CrSettingsPrefs.deferInitialization) |
| 154 }, | 156 this.initialize(); |
| 155 | 157 }, |
| 156 /** @override */ | 158 |
| 157 detached: function() { | 159 /** @override */ |
| 158 CrSettingsPrefs.resetForTesting(); | 160 detached: function() { |
| 159 }, | 161 CrSettingsPrefs.resetForTesting(); |
| 160 | 162 }, |
| 161 /** | 163 |
| 162 * @param {SettingsPrivate=} opt_settingsApi SettingsPrivate implementation | 164 /** |
| 163 * to use (chrome.settingsPrivate by default). | 165 * @param {SettingsPrivate=} opt_settingsApi SettingsPrivate implementation |
| 164 */ | 166 * to use (chrome.settingsPrivate by default). |
| 165 initialize: function(opt_settingsApi) { | 167 */ |
| 166 // Only initialize once (or after resetForTesting() is called). | 168 initialize: function(opt_settingsApi) { |
| 167 if (this.initialized_) | 169 // Only initialize once (or after resetForTesting() is called). |
| 168 return; | 170 if (this.initialized_) |
| 169 this.initialized_ = true; | 171 return; |
| 170 | 172 this.initialized_ = true; |
| 171 if (opt_settingsApi) | 173 |
| 172 this.settingsApi_ = opt_settingsApi; | 174 if (opt_settingsApi) |
| 173 | 175 this.settingsApi_ = opt_settingsApi; |
| 174 /** @private {function(!Array<!chrome.settingsPrivate.PrefObject>)} */ | 176 |
| 175 this.boundPrefsChanged_ = this.onSettingsPrivatePrefsChanged_.bind(this); | 177 /** @private {function(!Array<!chrome.settingsPrivate.PrefObject>)} */ |
| 176 this.settingsApi_.onPrefsChanged.addListener(this.boundPrefsChanged_); | 178 this.boundPrefsChanged_ = this.onSettingsPrivatePrefsChanged_.bind(this); |
| 177 this.settingsApi_.getAllPrefs( | 179 this.settingsApi_.onPrefsChanged.addListener(this.boundPrefsChanged_); |
| 178 this.onSettingsPrivatePrefsFetched_.bind(this)); | 180 this.settingsApi_.getAllPrefs( |
| 179 }, | 181 this.onSettingsPrivatePrefsFetched_.bind(this)); |
| 180 | 182 }, |
| 181 /** | 183 |
| 182 * @param {!{path: string}} e | 184 /** |
| 183 * @private | 185 * @param {!{path: string}} e |
| 184 */ | 186 * @private |
| 185 prefsChanged_: function(e) { | 187 */ |
| 186 // |prefs| can be directly set or unset in tests. | 188 prefsChanged_: function(e) { |
| 187 if (!CrSettingsPrefs.isInitialized || e.path == 'prefs') | 189 // |prefs| can be directly set or unset in tests. |
| 188 return; | 190 if (!CrSettingsPrefs.isInitialized || e.path == 'prefs') |
| 189 | 191 return; |
| 190 var key = this.getPrefKeyFromPath_(e.path); | 192 |
| 191 var prefStoreValue = this.lastPrefValues_[key]; | 193 var key = this.getPrefKeyFromPath_(e.path); |
| 192 | 194 var prefStoreValue = this.lastPrefValues_[key]; |
| 193 var prefObj = /** @type {chrome.settingsPrivate.PrefObject} */( | 195 |
| 194 this.get(key, this.prefs)); | 196 var prefObj = /** @type {chrome.settingsPrivate.PrefObject} */ ( |
| 195 | 197 this.get(key, this.prefs)); |
| 196 // If settingsPrivate already has this value, ignore it. (Otherwise, | 198 |
| 197 // a change event from settingsPrivate could make us call | 199 // If settingsPrivate already has this value, ignore it. (Otherwise, |
| 198 // settingsPrivate.setPref and potentially trigger an IPC loop.) | 200 // a change event from settingsPrivate could make us call |
| 199 if (!deepEqual(prefStoreValue, prefObj.value)) { | 201 // settingsPrivate.setPref and potentially trigger an IPC loop.) |
| 200 this.settingsApi_.setPref( | 202 if (!deepEqual(prefStoreValue, prefObj.value)) { |
| 201 key, | 203 this.settingsApi_.setPref( |
| 202 prefObj.value, | 204 key, prefObj.value, |
| 203 /* pageId */ '', | 205 /* pageId */ '', |
| 204 /* callback */ this.setPrefCallback_.bind(this, key)); | 206 /* callback */ this.setPrefCallback_.bind(this, key)); |
| 207 } |
| 208 }, |
| 209 |
| 210 /** |
| 211 * Called when prefs in the underlying Chrome pref store are changed. |
| 212 * @param {!Array<!chrome.settingsPrivate.PrefObject>} prefs |
| 213 * The prefs that changed. |
| 214 * @private |
| 215 */ |
| 216 onSettingsPrivatePrefsChanged_: function(prefs) { |
| 217 if (CrSettingsPrefs.isInitialized) |
| 218 this.updatePrefs_(prefs); |
| 219 }, |
| 220 |
| 221 /** |
| 222 * Called when prefs are fetched from settingsPrivate. |
| 223 * @param {!Array<!chrome.settingsPrivate.PrefObject>} prefs |
| 224 * @private |
| 225 */ |
| 226 onSettingsPrivatePrefsFetched_: function(prefs) { |
| 227 this.updatePrefs_(prefs); |
| 228 CrSettingsPrefs.setInitialized(); |
| 229 }, |
| 230 |
| 231 /** |
| 232 * Checks the result of calling settingsPrivate.setPref. |
| 233 * @param {string} key The key used in the call to setPref. |
| 234 * @param {boolean} success True if setting the pref succeeded. |
| 235 * @private |
| 236 */ |
| 237 setPrefCallback_: function(key, success) { |
| 238 if (!success) |
| 239 this.refresh(key); |
| 240 }, |
| 241 |
| 242 /** |
| 243 * Get the current pref value from chrome.settingsPrivate to ensure the UI |
| 244 * stays up to date. |
| 245 * @param {string} key |
| 246 */ |
| 247 refresh: function(key) { |
| 248 this.settingsApi_.getPref(key, function(pref) { |
| 249 this.updatePrefs_([pref]); |
| 250 }.bind(this)); |
| 251 }, |
| 252 |
| 253 /** |
| 254 * Updates the prefs model with the given prefs. |
| 255 * @param {!Array<!chrome.settingsPrivate.PrefObject>} newPrefs |
| 256 * @private |
| 257 */ |
| 258 updatePrefs_: function(newPrefs) { |
| 259 // Use the existing prefs object or create it. |
| 260 var prefs = this.prefs || {}; |
| 261 newPrefs.forEach(function(newPrefObj) { |
| 262 // Use the PrefObject from settingsPrivate to create a copy in |
| 263 // lastPrefValues_ at the pref's key. |
| 264 this.lastPrefValues_[newPrefObj.key] = deepCopy(newPrefObj.value); |
| 265 |
| 266 if (!deepEqual(this.get(newPrefObj.key, prefs), newPrefObj)) { |
| 267 // Add the pref to |prefs|. |
| 268 cr.exportPath(newPrefObj.key, newPrefObj, prefs); |
| 269 // If this.prefs already exists, notify listeners of the change. |
| 270 if (prefs == this.prefs) |
| 271 this.notifyPath('prefs.' + newPrefObj.key, newPrefObj); |
| 205 } | 272 } |
| 206 }, | 273 }, this); |
| 207 | 274 if (!this.prefs) |
| 208 /** | 275 this.prefs = prefs; |
| 209 * Called when prefs in the underlying Chrome pref store are changed. | 276 }, |
| 210 * @param {!Array<!chrome.settingsPrivate.PrefObject>} prefs | 277 |
| 211 * The prefs that changed. | 278 /** |
| 212 * @private | 279 * Given a 'property-changed' path, returns the key of the preference the |
| 213 */ | 280 * path refers to. E.g., if the path of the changed property is |
| 214 onSettingsPrivatePrefsChanged_: function(prefs) { | 281 * 'prefs.search.suggest_enabled.value', the key of the pref that changed is |
| 215 if (CrSettingsPrefs.isInitialized) | 282 * 'search.suggest_enabled'. |
| 216 this.updatePrefs_(prefs); | 283 * @param {string} path |
| 217 }, | 284 * @return {string} |
| 218 | 285 * @private |
| 219 /** | 286 */ |
| 220 * Called when prefs are fetched from settingsPrivate. | 287 getPrefKeyFromPath_: function(path) { |
| 221 * @param {!Array<!chrome.settingsPrivate.PrefObject>} prefs | 288 // Skip the first token, which refers to the member variable (this.prefs). |
| 222 * @private | 289 var parts = path.split('.'); |
| 223 */ | 290 assert(parts.shift() == 'prefs', 'Path doesn\'t begin with \'prefs\''); |
| 224 onSettingsPrivatePrefsFetched_: function(prefs) { | 291 |
| 225 this.updatePrefs_(prefs); | 292 for (var i = 1; i <= parts.length; i++) { |
| 226 CrSettingsPrefs.setInitialized(); | 293 var key = parts.slice(0, i).join('.'); |
| 227 }, | 294 // The lastPrefValues_ keys match the pref keys. |
| 228 | 295 if (this.lastPrefValues_.hasOwnProperty(key)) |
| 229 /** | 296 return key; |
| 230 * Checks the result of calling settingsPrivate.setPref. | 297 } |
| 231 * @param {string} key The key used in the call to setPref. | 298 return ''; |
| 232 * @param {boolean} success True if setting the pref succeeded. | 299 }, |
| 233 * @private | 300 |
| 234 */ | 301 /** |
| 235 setPrefCallback_: function(key, success) { | 302 * Resets the element so it can be re-initialized with a new prefs state. |
| 236 if (!success) | 303 */ |
| 237 this.refresh(key); | 304 resetForTesting: function() { |
| 238 }, | 305 if (!this.initialized_) |
| 239 | 306 return; |
| 240 /** | 307 this.prefs = undefined; |
| 241 * Get the current pref value from chrome.settingsPrivate to ensure the UI | 308 this.lastPrefValues_ = {}; |
| 242 * stays up to date. | 309 this.initialized_ = false; |
| 243 * @param {string} key | 310 // Remove the listener added in initialize(). |
| 244 */ | 311 this.settingsApi_.onPrefsChanged.removeListener(this.boundPrefsChanged_); |
| 245 refresh: function(key) { | 312 this.settingsApi_ = |
| 246 this.settingsApi_.getPref(key, function(pref) { | 313 /** @type {SettingsPrivate} */ (chrome.settingsPrivate); |
| 247 this.updatePrefs_([pref]); | 314 }, |
| 248 }.bind(this)); | 315 }); |
| 249 }, | |
| 250 | |
| 251 /** | |
| 252 * Updates the prefs model with the given prefs. | |
| 253 * @param {!Array<!chrome.settingsPrivate.PrefObject>} newPrefs | |
| 254 * @private | |
| 255 */ | |
| 256 updatePrefs_: function(newPrefs) { | |
| 257 // Use the existing prefs object or create it. | |
| 258 var prefs = this.prefs || {}; | |
| 259 newPrefs.forEach(function(newPrefObj) { | |
| 260 // Use the PrefObject from settingsPrivate to create a copy in | |
| 261 // lastPrefValues_ at the pref's key. | |
| 262 this.lastPrefValues_[newPrefObj.key] = deepCopy(newPrefObj.value); | |
| 263 | |
| 264 if (!deepEqual(this.get(newPrefObj.key, prefs), newPrefObj)) { | |
| 265 // Add the pref to |prefs|. | |
| 266 cr.exportPath(newPrefObj.key, newPrefObj, prefs); | |
| 267 // If this.prefs already exists, notify listeners of the change. | |
| 268 if (prefs == this.prefs) | |
| 269 this.notifyPath('prefs.' + newPrefObj.key, newPrefObj); | |
| 270 } | |
| 271 }, this); | |
| 272 if (!this.prefs) | |
| 273 this.prefs = prefs; | |
| 274 }, | |
| 275 | |
| 276 /** | |
| 277 * Given a 'property-changed' path, returns the key of the preference the | |
| 278 * path refers to. E.g., if the path of the changed property is | |
| 279 * 'prefs.search.suggest_enabled.value', the key of the pref that changed is | |
| 280 * 'search.suggest_enabled'. | |
| 281 * @param {string} path | |
| 282 * @return {string} | |
| 283 * @private | |
| 284 */ | |
| 285 getPrefKeyFromPath_: function(path) { | |
| 286 // Skip the first token, which refers to the member variable (this.prefs). | |
| 287 var parts = path.split('.'); | |
| 288 assert(parts.shift() == 'prefs', "Path doesn't begin with 'prefs'"); | |
| 289 | |
| 290 for (var i = 1; i <= parts.length; i++) { | |
| 291 var key = parts.slice(0, i).join('.'); | |
| 292 // The lastPrefValues_ keys match the pref keys. | |
| 293 if (this.lastPrefValues_.hasOwnProperty(key)) | |
| 294 return key; | |
| 295 } | |
| 296 return ''; | |
| 297 }, | |
| 298 | |
| 299 /** | |
| 300 * Resets the element so it can be re-initialized with a new prefs state. | |
| 301 */ | |
| 302 resetForTesting: function() { | |
| 303 if (!this.initialized_) | |
| 304 return; | |
| 305 this.prefs = undefined; | |
| 306 this.lastPrefValues_ = {}; | |
| 307 this.initialized_ = false; | |
| 308 // Remove the listener added in initialize(). | |
| 309 this.settingsApi_.onPrefsChanged.removeListener(this.boundPrefsChanged_); | |
| 310 this.settingsApi_ = | |
| 311 /** @type {SettingsPrivate} */(chrome.settingsPrivate); | |
| 312 }, | |
| 313 }); | |
| 314 })(); | 316 })(); |
| OLD | NEW |