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 |