1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 * The drive mount path used in the storage. It must be '/drive'.
9 var STORED_DRIVE_MOUNT_PATH = '/drive';
12 * Model for the folder shortcuts. This object is cr.ui.ArrayDataModel-like
13 * object with additional methods for the folder shortcut feature.
14 * This uses chrome.storage as backend. Items are always sorted by URL.
16 * @param {VolumeManagerWrapper} volumeManager Volume manager instance.
18 * @extends {cr.EventTarget}
20 function FolderShortcutsDataModel(volumeManager) {
21 this.volumeManager_ = volumeManager;
23 this.pendingPaths_ = {}; // Hash map for easier deleting.
24 this.unresolvablePaths_ = {};
25 this.lastDriveRootURL_ = null;
27 // Queue to serialize resolving entries.
28 this.queue_ = new AsyncUtil.Queue();
29 this.queue_.run(this.volumeManager_.ensureInitialized.bind(this));
31 // Load the shortcuts. Runs within the queue.
34 // Listening for changes in the storage.
35 chrome.storage.onChanged.addListener(function(changes, namespace) {
36 if (!(FolderShortcutsDataModel.NAME in changes) || namespace !== 'sync')
38 this.reload_(); // Runs within the queue.
41 // If the volume info list is changed, then shortcuts have to be reloaded.
42 this.volumeManager_.volumeInfoList.addEventListener(
43 'permuted', this.reload_.bind(this));
45 // If the drive status has changed, then shortcuts have to be re-resolved.
46 this.volumeManager_.addEventListener(
47 'drive-connection-changed', this.reload_.bind(this));
51 * Key name in chrome.storage. The array are stored with this name.
55 FolderShortcutsDataModel.NAME = 'folder-shortcuts-list';
57 FolderShortcutsDataModel.prototype = {
58 __proto__: cr.EventTarget.prototype,
61 * @return {number} Number of elements in the array.
64 return this.array_.length;
68 * Remembers the Drive volume's root URL used for conversions between virtual
72 rememberLastDriveURL_: function() {
73 if (this.lastDriveRootURL_)
75 var volumeInfo = this.volumeManager_.getCurrentProfileVolumeInfo(
76 util.VolumeType.DRIVE);
78 this.lastDriveRootURL_ = volumeInfo.fileSystem.root.toURL();
82 * Resolves Entries from a list of stored virtual paths. Runs within a queue.
83 * @param {Array.<string>} list List of virtual paths.
86 processEntries_: function(list) {
87 this.queue_.run(function(callback) {
88 this.pendingPaths_ = {};
89 this.unresolvablePaths_ = {};
90 list.forEach(function(path) {
91 this.pendingPaths_[path] = true;
96 this.queue_.run(function(queueCallback) {
97 var volumeInfo = this.volumeManager_.getCurrentProfileVolumeInfo(
98 util.VolumeType.DRIVE);
100 var resolvedURLs = {};
101 this.rememberLastDriveURL_(); // Required for conversions.
103 var onResolveSuccess = function(path, entry) {
104 if (path in this.pendingPaths_)
105 delete this.pendingPaths_[path];
106 if (path in this.unresolvablePaths_) {
108 delete this.unresolvablePaths_[path];
110 if (!this.exists(entry)) {
112 this.addInternal_(entry);
114 resolvedURLs[entry.toURL()] = true;
117 var onResolveFailure = function(path, url) {
118 if (path in this.pendingPaths_)
119 delete this.pendingPaths_[path];
120 var existingIndex = this.getIndexByURL_(url);
121 if (existingIndex !== -1) {
123 this.removeInternal_(this.item(existingIndex));
125 // Remove the shortcut on error, only if Drive is fully online.
126 // Only then we can be sure, that the error means that the directory
127 // does not exist anymore.
129 this.volumeManager_.getDriveConnectionState().type !==
130 util.DriveConnectionType.ONLINE) {
131 if (!this.unresolvablePaths_[path]) {
133 this.unresolvablePaths_[path] = true;
136 // Not adding to the model nor to the |unresolvablePaths_| means
137 // that it will be removed from the storage permanently after the
138 // next call to save_().
141 // Resolve the items all at once, in parallel.
142 var group = new AsyncUtil.Group();
143 list.forEach(function(path) {
144 group.add(function(path, callback) {
146 this.lastDriveRootURL_ && this.convertStoredPathToUrl_(path);
147 if (url && volumeInfo) {
148 webkitResolveLocalFileSystemURL(
151 onResolveSuccess(path, entry);
155 onResolveFailure(path, url);
159 onResolveFailure(path, url);
165 // Save the model after finishing.
166 group.run(function() {
167 // Remove all of those old entries, which were resolved by this method.
169 while (index < this.length) {
170 var entry = this.item(index);
171 if (!resolvedURLs[entry.toURL()]) {
172 this.removeInternal_(entry);
178 // If something changed, then save.
187 * Initializes the model and loads the shortcuts.
191 this.queue_.run(function(callback) {
192 chrome.storage.sync.get(FolderShortcutsDataModel.NAME, function(value) {
193 var shortcutPaths = value[FolderShortcutsDataModel.NAME] || [];
196 metrics.recordSmallCount('FolderShortcut.Count', shortcutPaths.length);
198 // Resolve and add the entries to the model.
199 this.processEntries_(shortcutPaths); // Runs within a queue.
206 * Reloads the model and loads the shortcuts.
209 reload_: function(ev) {
211 this.queue_.run(function(callback) {
212 chrome.storage.sync.get(FolderShortcutsDataModel.NAME, function(value) {
213 var shortcutPaths = value[FolderShortcutsDataModel.NAME] || [];
214 this.processEntries_(shortcutPaths); // Runs within a queue.
221 * Returns the entries in the given range as a new array instance. The
222 * arguments and return value are compatible with Array.slice().
224 * @param {number} start Where to start the selection.
225 * @param {number=} opt_end Where to end the selection.
226 * @return {Array.<Entry>} Entries in the selected range.
228 slice: function(begin, opt_end) {
229 return this.array_.slice(begin, opt_end);
233 * @param {number} index Index of the element to be retrieved.
234 * @return {Entry} The value of the |index|-th element.
236 item: function(index) {
237 return this.array_[index];
241 * @param {string} value URL of the entry to be found.
242 * @return {number} Index of the element with the specified |value|.
245 getIndexByURL_: function(value) {
246 for (var i = 0; i < this.length; i++) {
247 // Same item check: must be exact match.
248 if (this.array_[i].toURL() === value)
255 * @param {Entry} value Value of the element to be retrieved.
256 * @return {number} Index of the element with the specified |value|.
258 getIndex: function(value) {
259 for (var i = 0; i < this.length; i++) {
260 // Same item check: must be exact match.
261 if (util.isSameEntry(this.array_[i], value))
268 * Compares 2 entries and returns a number indicating one entry comes before
269 * or after or is the same as the other entry in sort order.
271 * @param {Entry} a First entry.
272 * @param {Entry} b Second entry.
273 * @return {boolean} Returns -1, if |a| < |b|. Returns 0, if |a| === |b|.
274 * Otherwise, returns 1.
276 compare: function(a, b) {
277 return a.toURL().localeCompare(
279 undefined, // locale parameter, use default locale.
280 {usage: 'sort', numeric: true});
284 * Adds the given item to the array. If there were already same item in the
285 * list, return the index of the existing item without adding a duplicate
288 * @param {Entry} value Value to be added into the array.
289 * @return {number} Index in the list which the element added to.
291 add: function(value) {
292 var result = this.addInternal_(value);
293 metrics.recordUserAction('FolderShortcut.Add');
299 * Adds the given item to the array. If there were already same item in the
300 * list, return the index of the existing item without adding a duplicate
303 * @param {Entry} value Value to be added into the array.
304 * @return {number} Index in the list which the element added to.
307 addInternal_: function(value) {
308 this.rememberLastDriveURL_(); // Required for saving.
310 var oldArray = this.array_.slice(0); // Shallow copy.
312 for (var i = 0; i < this.length; i++) {
313 // Same item check: must be exact match.
314 if (util.isSameEntry(this.array_[i], value))
317 // Since the array is sorted, new item will be added just before the first
319 if (this.compare(this.array_[i], value) >= 0) {
320 this.array_.splice(i, 0, value);
325 // If value is not added yet, add it at the last.
326 if (addedIndex == -1) {
327 this.array_.push(value);
328 addedIndex = this.length;
331 this.firePermutedEvent_(
332 this.calculatePermutation_(oldArray, this.array_));
337 * Removes the given item from the array.
338 * @param {Entry} value Value to be removed from the array.
339 * @return {number} Index in the list which the element removed from.
341 remove: function(value) {
342 var result = this.removeInternal_(value);
345 metrics.recordUserAction('FolderShortcut.Remove');
351 * Removes the given item from the array.
353 * @param {Entry} value Value to be removed from the array.
354 * @return {number} Index in the list which the element removed from.
357 removeInternal_: function(value) {
358 var removedIndex = -1;
359 var oldArray = this.array_.slice(0); // Shallow copy.
360 for (var i = 0; i < this.length; i++) {
361 // Same item check: must be exact match.
362 if (util.isSameEntry(this.array_[i], value)) {
363 this.array_.splice(i, 1);
369 if (removedIndex !== -1) {
370 this.firePermutedEvent_(
371 this.calculatePermutation_(oldArray, this.array_));
375 // No item is removed.
380 * @param {Entry} entry Entry to be checked.
381 * @return {boolean} True if the given |entry| exists in the array. False
384 exists: function(entry) {
385 var index = this.getIndex(entry);
390 * Saves the current array to chrome.storage.
394 // The current implementation doesn't rely on sort order in prefs, however
395 // older versions do. Therefore, we need to sort the paths before saving.
396 // TODO(mtomasz): Remove sorting prefs after M-34 is stable.
398 var compareByPath = function(a, b) {
399 return a.localeCompare(
401 undefined, // locale parameter, use default locale.
402 {usage: 'sort', numeric: true});
405 this.rememberLastDriveURL_();
406 if (!this.lastDriveRootURL_)
409 // TODO(mtomasz): Migrate to URL.
410 var paths = this.array_.
411 map(function(entry) { return entry.toURL(); }).
412 map(this.convertUrlToStoredPath_.bind(this)).
413 concat(Object.keys(this.pendingPaths_)).
414 concat(Object.keys(this.unresolvablePaths_)).
418 prefs[FolderShortcutsDataModel.NAME] = paths;
419 chrome.storage.sync.set(prefs, function() {});
423 * Creates a permutation array for 'permuted' event, which is compatible with
424 * a permutation array used in cr/ui/array_data_model.js.
426 * @param {array} oldArray Previous array before changing.
427 * @param {array} newArray New array after changing.
428 * @return {Array.<number>} Created permutation array.
431 calculatePermutation_: function(oldArray, newArray) {
432 var oldIndex = 0; // Index of oldArray.
433 var newIndex = 0; // Index of newArray.
435 // Note that both new and old arrays are sorted.
436 var permutation = [];
437 for (; oldIndex < oldArray.length; oldIndex++) {
438 if (newIndex >= newArray.length) {
439 // oldArray[oldIndex] is deleted, which is not in the new array.
440 permutation[oldIndex] = -1;
444 while (newIndex < newArray.length) {
445 // Unchanged item, which exists in both new and old array. But the
446 // index may be changed.
447 if (util.isSameEntry(oldArray[oldIndex], newArray[newIndex])) {
448 permutation[oldIndex] = newIndex;
453 // oldArray[oldIndex] is deleted, which is not in the new array.
454 if (this.compare(oldArray[oldIndex], newArray[newIndex]) < 0) {
455 permutation[oldIndex] = -1;
459 // In the case of this.compare(oldArray[oldIndex]) > 0:
460 // newArray[newIndex] is added, which is not in the old array.
468 * Fires a 'permuted' event, which is compatible with cr.ui.ArrayDataModel.
469 * @param {Array.<number>} Permutation array.
471 firePermutedEvent_: function(permutation) {
472 var permutedEvent = new Event('permuted');
473 permutedEvent.newLength = this.length;
474 permutedEvent.permutation = permutation;
475 this.dispatchEvent(permutedEvent);
477 // Note: This model only fires 'permuted' event, because:
478 // 1) 'change' event is not necessary to fire since it is covered by
480 // 2) 'splice' and 'sorted' events are not implemented. These events are
481 // not used in NavigationListModel. We have to implement them when
486 * Called externally when one of the items is not found on the filesystem.
487 * @param {Entry} entry The entry which is not found.
489 onItemNotFoundError: function(entry) {
490 // If Drive is online, then delete the shortcut permanently. Otherwise,
491 // delete from model and add to |unresolvablePaths_|.
492 if (this.volumeManager_.getDriveConnectionState().type !==
493 util.DriveConnectionType.ONLINE) {
494 var path = this.convertUrlToStoredPath_(entry.toURL());
495 // TODO(mtomasz): Add support for multi-profile.
496 this.unresolvablePaths_[path] = true;
498 this.removeInternal_(entry);
503 * Converts the given "stored path" to the URL.
505 * This conversion is necessary because the shortcuts are not stored with
506 * stored-formatted mount paths for compatibility. See http://crbug.com/336155
509 * @param {string} path Path in Drive with the stored drive mount path.
510 * @return {string} URL of the given path.
513 convertStoredPathToUrl_: function(path) {
514 if (path.indexOf(STORED_DRIVE_MOUNT_PATH + '/') !== 0) {
515 console.warn(path + ' is neither a drive mount path nor a stored path.');
518 return this.lastDriveRootURL_ + encodeURIComponent(
519 path.substr(STORED_DRIVE_MOUNT_PATH.length));
523 * Converts the URL to the stored-formatted path.
525 * See the comment of convertStoredPathToUrl_() for further information.
527 * @param {string} url URL of the directory in Drive.
528 * @return {string} Path with the stored drive mount path.
531 convertUrlToStoredPath_: function(url) {
532 // Root URLs contain a trailing slash.
533 if (url.indexOf(this.lastDriveRootURL_) !== 0) {
534 console.warn(url + ' is not a drive URL.');
538 return STORED_DRIVE_MOUNT_PATH + '/' + decodeURIComponent(
539 url.substr(this.lastDriveRootURL_.length));