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();
30 this.volumeManager_.ensureInitialized.bind(this.volumeManager_));
32 // Load the shortcuts. Runs within the queue.
35 // Listening for changes in the storage.
36 chrome.storage.onChanged.addListener(function(changes, namespace) {
37 if (!(FolderShortcutsDataModel.NAME in changes) || namespace !== 'sync')
39 this.reload_(); // Runs within the queue.
42 // If the volume info list is changed, then shortcuts have to be reloaded.
43 this.volumeManager_.volumeInfoList.addEventListener(
44 'permuted', this.reload_.bind(this));
46 // If the drive status has changed, then shortcuts have to be re-resolved.
47 this.volumeManager_.addEventListener(
48 'drive-connection-changed', this.reload_.bind(this));
52 * Key name in chrome.storage. The array are stored with this name.
56 FolderShortcutsDataModel.NAME = 'folder-shortcuts-list';
58 FolderShortcutsDataModel.prototype = {
59 __proto__: cr.EventTarget.prototype,
62 * @return {number} Number of elements in the array.
65 return this.array_.length;
69 * Remembers the Drive volume's root URL used for conversions between virtual
73 rememberLastDriveURL_: function() {
74 if (this.lastDriveRootURL_)
76 var volumeInfo = this.volumeManager_.getCurrentProfileVolumeInfo(
77 VolumeManagerCommon.VolumeType.DRIVE);
79 this.lastDriveRootURL_ = volumeInfo.fileSystem.root.toURL();
83 * Resolves Entries from a list of stored virtual paths. Runs within a queue.
84 * @param {Array.<string>} list List of virtual paths.
87 processEntries_: function(list) {
88 this.queue_.run(function(callback) {
89 this.pendingPaths_ = {};
90 this.unresolvablePaths_ = {};
91 list.forEach(function(path) {
92 this.pendingPaths_[path] = true;
97 this.queue_.run(function(queueCallback) {
98 var volumeInfo = this.volumeManager_.getCurrentProfileVolumeInfo(
99 VolumeManagerCommon.VolumeType.DRIVE);
101 var resolvedURLs = {};
102 this.rememberLastDriveURL_(); // Required for conversions.
104 var onResolveSuccess = function(path, entry) {
105 if (path in this.pendingPaths_)
106 delete this.pendingPaths_[path];
107 if (path in this.unresolvablePaths_) {
109 delete this.unresolvablePaths_[path];
111 if (!this.exists(entry)) {
113 this.addInternal_(entry);
115 resolvedURLs[entry.toURL()] = true;
118 var onResolveFailure = function(path, url) {
119 if (path in this.pendingPaths_)
120 delete this.pendingPaths_[path];
121 var existingIndex = this.getIndexByURL_(url);
122 if (existingIndex !== -1) {
124 this.removeInternal_(this.item(existingIndex));
126 // Remove the shortcut on error, only if Drive is fully online.
127 // Only then we can be sure, that the error means that the directory
128 // does not exist anymore.
130 this.volumeManager_.getDriveConnectionState().type !==
131 VolumeManagerCommon.DriveConnectionType.ONLINE) {
132 if (!this.unresolvablePaths_[path]) {
134 this.unresolvablePaths_[path] = true;
137 // Not adding to the model nor to the |unresolvablePaths_| means
138 // that it will be removed from the storage permanently after the
139 // next call to save_().
142 // Resolve the items all at once, in parallel.
143 var group = new AsyncUtil.Group();
144 list.forEach(function(path) {
145 group.add(function(path, callback) {
147 this.lastDriveRootURL_ && this.convertStoredPathToUrl_(path);
148 if (url && volumeInfo) {
149 webkitResolveLocalFileSystemURL(
152 onResolveSuccess(path, entry);
156 onResolveFailure(path, url);
160 onResolveFailure(path, url);
166 // Save the model after finishing.
167 group.run(function() {
168 // Remove all of those old entries, which were resolved by this method.
170 while (index < this.length) {
171 var entry = this.item(index);
172 if (!resolvedURLs[entry.toURL()]) {
173 this.removeInternal_(entry);
179 // If something changed, then save.
188 * Initializes the model and loads the shortcuts.
192 this.queue_.run(function(callback) {
193 chrome.storage.sync.get(FolderShortcutsDataModel.NAME, function(value) {
194 var shortcutPaths = value[FolderShortcutsDataModel.NAME] || [];
197 metrics.recordSmallCount('FolderShortcut.Count', shortcutPaths.length);
199 // Resolve and add the entries to the model.
200 this.processEntries_(shortcutPaths); // Runs within a queue.
207 * Reloads the model and loads the shortcuts.
210 reload_: function(ev) {
212 this.queue_.run(function(callback) {
213 chrome.storage.sync.get(FolderShortcutsDataModel.NAME, function(value) {
214 var shortcutPaths = value[FolderShortcutsDataModel.NAME] || [];
215 this.processEntries_(shortcutPaths); // Runs within a queue.
222 * Returns the entries in the given range as a new array instance. The
223 * arguments and return value are compatible with Array.slice().
225 * @param {number} begin Where to start the selection.
226 * @param {number=} opt_end Where to end the selection.
227 * @return {Array.<Entry>} Entries in the selected range.
229 slice: function(begin, opt_end) {
230 return this.array_.slice(begin, opt_end);
234 * @param {number} index Index of the element to be retrieved.
235 * @return {Entry} The value of the |index|-th element.
237 item: function(index) {
238 return this.array_[index];
242 * @param {string} value URL of the entry to be found.
243 * @return {number} Index of the element with the specified |value|.
246 getIndexByURL_: function(value) {
247 for (var i = 0; i < this.length; i++) {
248 // Same item check: must be exact match.
249 if (this.array_[i].toURL() === value)
256 * @param {Entry} value Value of the element to be retrieved.
257 * @return {number} Index of the element with the specified |value|.
259 getIndex: function(value) {
260 for (var i = 0; i < this.length; i++) {
261 // Same item check: must be exact match.
262 if (util.isSameEntry(this.array_[i], value))
269 * Compares 2 entries and returns a number indicating one entry comes before
270 * or after or is the same as the other entry in sort order.
272 * @param {Entry} a First entry.
273 * @param {Entry} b Second entry.
274 * @return {boolean} Returns -1, if |a| < |b|. Returns 0, if |a| === |b|.
275 * Otherwise, returns 1.
277 compare: function(a, b) {
278 return util.comparePath(a, b);
282 * Adds the given item to the array. If there were already same item in the
283 * list, return the index of the existing item without adding a duplicate
286 * @param {Entry} value Value to be added into the array.
287 * @return {number} Index in the list which the element added to.
289 add: function(value) {
290 var result = this.addInternal_(value);
291 metrics.recordUserAction('FolderShortcut.Add');
297 * Adds the given item to the array. If there were already same item in the
298 * list, return the index of the existing item without adding a duplicate
301 * @param {Entry} value Value to be added into the array.
302 * @return {number} Index in the list which the element added to.
305 addInternal_: function(value) {
306 this.rememberLastDriveURL_(); // Required for saving.
308 var oldArray = this.array_.slice(0); // Shallow copy.
310 for (var i = 0; i < this.length; i++) {
311 // Same item check: must be exact match.
312 if (util.isSameEntry(this.array_[i], value))
315 // Since the array is sorted, new item will be added just before the first
317 if (this.compare(this.array_[i], value) >= 0) {
318 this.array_.splice(i, 0, value);
323 // If value is not added yet, add it at the last.
324 if (addedIndex == -1) {
325 this.array_.push(value);
326 addedIndex = this.length;
329 this.firePermutedEvent_(
330 this.calculatePermutation_(oldArray, this.array_));
335 * Removes the given item from the array.
336 * @param {Entry} value Value to be removed from the array.
337 * @return {number} Index in the list which the element removed from.
339 remove: function(value) {
340 var result = this.removeInternal_(value);
343 metrics.recordUserAction('FolderShortcut.Remove');
349 * Removes the given item from the array.
351 * @param {Entry} value Value to be removed from the array.
352 * @return {number} Index in the list which the element removed from.
355 removeInternal_: function(value) {
356 var removedIndex = -1;
357 var oldArray = this.array_.slice(0); // Shallow copy.
358 for (var i = 0; i < this.length; i++) {
359 // Same item check: must be exact match.
360 if (util.isSameEntry(this.array_[i], value)) {
361 this.array_.splice(i, 1);
367 if (removedIndex !== -1) {
368 this.firePermutedEvent_(
369 this.calculatePermutation_(oldArray, this.array_));
373 // No item is removed.
378 * @param {Entry} entry Entry to be checked.
379 * @return {boolean} True if the given |entry| exists in the array. False
382 exists: function(entry) {
383 var index = this.getIndex(entry);
388 * Saves the current array to chrome.storage.
392 this.rememberLastDriveURL_();
393 if (!this.lastDriveRootURL_)
396 // TODO(mtomasz): Migrate to URL.
397 var paths = this.array_.
398 map(function(entry) { return entry.toURL(); }).
399 map(this.convertUrlToStoredPath_.bind(this)).
400 concat(Object.keys(this.pendingPaths_)).
401 concat(Object.keys(this.unresolvablePaths_));
404 prefs[FolderShortcutsDataModel.NAME] = paths;
405 chrome.storage.sync.set(prefs, function() {});
409 * Creates a permutation array for 'permuted' event, which is compatible with
410 * a permutation array used in cr/ui/array_data_model.js.
412 * @param {array} oldArray Previous array before changing.
413 * @param {array} newArray New array after changing.
414 * @return {Array.<number>} Created permutation array.
417 calculatePermutation_: function(oldArray, newArray) {
418 var oldIndex = 0; // Index of oldArray.
419 var newIndex = 0; // Index of newArray.
421 // Note that both new and old arrays are sorted.
422 var permutation = [];
423 for (; oldIndex < oldArray.length; oldIndex++) {
424 if (newIndex >= newArray.length) {
425 // oldArray[oldIndex] is deleted, which is not in the new array.
426 permutation[oldIndex] = -1;
430 while (newIndex < newArray.length) {
431 // Unchanged item, which exists in both new and old array. But the
432 // index may be changed.
433 if (util.isSameEntry(oldArray[oldIndex], newArray[newIndex])) {
434 permutation[oldIndex] = newIndex;
439 // oldArray[oldIndex] is deleted, which is not in the new array.
440 if (this.compare(oldArray[oldIndex], newArray[newIndex]) < 0) {
441 permutation[oldIndex] = -1;
445 // In the case of this.compare(oldArray[oldIndex]) > 0:
446 // newArray[newIndex] is added, which is not in the old array.
454 * Fires a 'permuted' event, which is compatible with cr.ui.ArrayDataModel.
455 * @param {Array.<number>} permutation Permutation array.
457 firePermutedEvent_: function(permutation) {
458 var permutedEvent = new Event('permuted');
459 permutedEvent.newLength = this.length;
460 permutedEvent.permutation = permutation;
461 this.dispatchEvent(permutedEvent);
463 // Note: This model only fires 'permuted' event, because:
464 // 1) 'change' event is not necessary to fire since it is covered by
466 // 2) 'splice' and 'sorted' events are not implemented. These events are
467 // not used in NavigationListModel. We have to implement them when
472 * Called externally when one of the items is not found on the filesystem.
473 * @param {Entry} entry The entry which is not found.
475 onItemNotFoundError: function(entry) {
476 // If Drive is online, then delete the shortcut permanently. Otherwise,
477 // delete from model and add to |unresolvablePaths_|.
478 if (this.volumeManager_.getDriveConnectionState().type !==
479 VolumeManagerCommon.DriveConnectionType.ONLINE) {
480 var path = this.convertUrlToStoredPath_(entry.toURL());
481 // TODO(mtomasz): Add support for multi-profile.
482 this.unresolvablePaths_[path] = true;
484 this.removeInternal_(entry);
489 * Converts the given "stored path" to the URL.
491 * This conversion is necessary because the shortcuts are not stored with
492 * stored-formatted mount paths for compatibility. See http://crbug.com/336155
495 * @param {string} path Path in Drive with the stored drive mount path.
496 * @return {string} URL of the given path.
499 convertStoredPathToUrl_: function(path) {
500 if (path.indexOf(STORED_DRIVE_MOUNT_PATH + '/') !== 0) {
501 console.warn(path + ' is neither a drive mount path nor a stored path.');
504 return this.lastDriveRootURL_ + encodeURIComponent(
505 path.substr(STORED_DRIVE_MOUNT_PATH.length));
509 * Converts the URL to the stored-formatted path.
511 * See the comment of convertStoredPathToUrl_() for further information.
513 * @param {string} url URL of the directory in Drive.
514 * @return {string} Path with the stored drive mount path.
517 convertUrlToStoredPath_: function(url) {
518 // Root URLs contain a trailing slash.
519 if (url.indexOf(this.lastDriveRootURL_) !== 0) {
520 console.warn(url + ' is not a drive URL.');
524 return STORED_DRIVE_MOUNT_PATH + '/' + decodeURIComponent(
525 url.substr(this.lastDriveRootURL_.length));