Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / file_manager / foreground / js / folder_shortcuts_data_model.js
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.
4
5 /**
6  * The drive mount path used in the storage. It must be '/drive'.
7  * @type {string}
8  */
9 var STORED_DRIVE_MOUNT_PATH = '/drive';
10
11 /**
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.
15  *
16  * @param {VolumeManagerWrapper} volumeManager Volume manager instance.
17  * @constructor
18  * @extends {cr.EventTarget}
19  */
20 function FolderShortcutsDataModel(volumeManager) {
21   this.volumeManager_ = volumeManager;
22   this.array_ = [];
23   this.pendingPaths_ = {};  // Hash map for easier deleting.
24   this.unresolvablePaths_ = {};
25   this.lastDriveRootURL_ = null;
26
27   // Queue to serialize resolving entries.
28   this.queue_ = new AsyncUtil.Queue();
29   this.queue_.run(
30       this.volumeManager_.ensureInitialized.bind(this.volumeManager_));
31
32   // Load the shortcuts. Runs within the queue.
33   this.load_();
34
35   // Listening for changes in the storage.
36   chrome.storage.onChanged.addListener(function(changes, namespace) {
37     if (!(FolderShortcutsDataModel.NAME in changes) || namespace !== 'sync')
38       return;
39     this.reload_();  // Runs within the queue.
40   }.bind(this));
41
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));
45
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));
49 }
50
51 /**
52  * Key name in chrome.storage. The array are stored with this name.
53  * @type {string}
54  * @const
55  */
56 FolderShortcutsDataModel.NAME = 'folder-shortcuts-list';
57
58 FolderShortcutsDataModel.prototype = {
59   __proto__: cr.EventTarget.prototype,
60
61   /**
62    * @return {number} Number of elements in the array.
63    */
64   get length() {
65     return this.array_.length;
66   },
67
68   /**
69    * Remembers the Drive volume's root URL used for conversions between virtual
70    * paths and URLs.
71    * @private
72    */
73   rememberLastDriveURL_: function() {
74     if (this.lastDriveRootURL_)
75       return;
76     var volumeInfo = this.volumeManager_.getCurrentProfileVolumeInfo(
77         VolumeManagerCommon.VolumeType.DRIVE);
78     if (volumeInfo)
79       this.lastDriveRootURL_ = volumeInfo.fileSystem.root.toURL();
80   },
81
82   /**
83    * Resolves Entries from a list of stored virtual paths. Runs within a queue.
84    * @param {Array.<string>} list List of virtual paths.
85    * @private
86    */
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;
93       }, this);
94       callback();
95     }.bind(this));
96
97     this.queue_.run(function(queueCallback) {
98       var volumeInfo = this.volumeManager_.getCurrentProfileVolumeInfo(
99           VolumeManagerCommon.VolumeType.DRIVE);
100       var changed = false;
101       var resolvedURLs = {};
102       this.rememberLastDriveURL_();  // Required for conversions.
103
104       var onResolveSuccess = function(path, entry) {
105         if (path in this.pendingPaths_)
106           delete this.pendingPaths_[path];
107         if (path in this.unresolvablePaths_) {
108           changed = true;
109           delete this.unresolvablePaths_[path];
110         }
111         if (!this.exists(entry)) {
112           changed = true;
113           this.addInternal_(entry);
114         }
115         resolvedURLs[entry.toURL()] = true;
116       }.bind(this);
117
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) {
123           changed = true;
124           this.removeInternal_(this.item(existingIndex));
125         }
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.
129         if (!volumeInfo ||
130             this.volumeManager_.getDriveConnectionState().type !==
131                 VolumeManagerCommon.DriveConnectionType.ONLINE) {
132           if (!this.unresolvablePaths_[path]) {
133             changed = true;
134             this.unresolvablePaths_[path] = true;
135           }
136         }
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_().
140       }.bind(this);
141
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) {
146           var url =
147               this.lastDriveRootURL_ && this.convertStoredPathToUrl_(path);
148           if (url && volumeInfo) {
149             webkitResolveLocalFileSystemURL(
150                 url,
151                 function(entry) {
152                   onResolveSuccess(path, entry);
153                   callback();
154                 },
155                 function() {
156                   onResolveFailure(path, url);
157                   callback();
158                 });
159           } else {
160             onResolveFailure(path, url);
161             callback();
162           }
163         }.bind(this, path));
164       }, this);
165
166       // Save the model after finishing.
167       group.run(function() {
168         // Remove all of those old entries, which were resolved by this method.
169         var index = 0;
170         while (index < this.length) {
171           var entry = this.item(index);
172           if (!resolvedURLs[entry.toURL()]) {
173             this.removeInternal_(entry);
174             changed = true;
175           } else {
176             index++;
177           }
178         }
179         // If something changed, then save.
180         if (changed)
181           this.save_();
182         queueCallback();
183       }.bind(this));
184     }.bind(this));
185   },
186
187   /**
188    * Initializes the model and loads the shortcuts.
189    * @private
190    */
191   load_: function() {
192     this.queue_.run(function(callback) {
193       chrome.storage.sync.get(FolderShortcutsDataModel.NAME, function(value) {
194         var shortcutPaths = value[FolderShortcutsDataModel.NAME] || [];
195
196         // Record metrics.
197         metrics.recordSmallCount('FolderShortcut.Count', shortcutPaths.length);
198
199         // Resolve and add the entries to the model.
200         this.processEntries_(shortcutPaths);  // Runs within a queue.
201         callback();
202       }.bind(this));
203     }.bind(this));
204   },
205
206   /**
207    * Reloads the model and loads the shortcuts.
208    * @private
209    */
210   reload_: function(ev) {
211     var shortcutPaths;
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.
216         callback();
217       }.bind(this));
218     }.bind(this));
219   },
220
221   /**
222    * Returns the entries in the given range as a new array instance. The
223    * arguments and return value are compatible with Array.slice().
224    *
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.
228    */
229   slice: function(begin, opt_end) {
230     return this.array_.slice(begin, opt_end);
231   },
232
233   /**
234    * @param {number} index Index of the element to be retrieved.
235    * @return {Entry} The value of the |index|-th element.
236    */
237   item: function(index) {
238     return this.array_[index];
239   },
240
241   /**
242    * @param {string} value URL of the entry to be found.
243    * @return {number} Index of the element with the specified |value|.
244    * @private
245    */
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)
250         return i;
251     }
252     return -1;
253   },
254
255   /**
256    * @param {Entry} value Value of the element to be retrieved.
257    * @return {number} Index of the element with the specified |value|.
258    */
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))
263         return i;
264     }
265     return -1;
266   },
267
268   /**
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.
271    *
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.
276    */
277   compare: function(a, b) {
278     return util.comparePath(a, b);
279   },
280
281   /**
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
284    * item.
285    *
286    * @param {Entry} value Value to be added into the array.
287    * @return {number} Index in the list which the element added to.
288    */
289   add: function(value) {
290     var result = this.addInternal_(value);
291     metrics.recordUserAction('FolderShortcut.Add');
292     this.save_();
293     return result;
294   },
295
296   /**
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
299    * item.
300    *
301    * @param {Entry} value Value to be added into the array.
302    * @return {number} Index in the list which the element added to.
303    * @private
304    */
305   addInternal_: function(value) {
306     this.rememberLastDriveURL_();  // Required for saving.
307
308     var oldArray = this.array_.slice(0);  // Shallow copy.
309     var addedIndex = -1;
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))
313         return i;
314
315       // Since the array is sorted, new item will be added just before the first
316       // larger item.
317       if (this.compare(this.array_[i], value) >= 0) {
318         this.array_.splice(i, 0, value);
319         addedIndex = i;
320         break;
321       }
322     }
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;
327     }
328
329     this.firePermutedEvent_(
330         this.calculatePermutation_(oldArray, this.array_));
331     return addedIndex;
332   },
333
334   /**
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.
338    */
339   remove: function(value) {
340     var result = this.removeInternal_(value);
341     if (result !== -1) {
342       this.save_();
343       metrics.recordUserAction('FolderShortcut.Remove');
344     }
345     return result;
346   },
347
348   /**
349    * Removes the given item from the array.
350    *
351    * @param {Entry} value Value to be removed from the array.
352    * @return {number} Index in the list which the element removed from.
353    * @private
354    */
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);
362         removedIndex = i;
363         break;
364       }
365     }
366
367     if (removedIndex !== -1) {
368       this.firePermutedEvent_(
369           this.calculatePermutation_(oldArray, this.array_));
370       return removedIndex;
371     }
372
373     // No item is removed.
374     return -1;
375   },
376
377   /**
378    * @param {Entry} entry Entry to be checked.
379    * @return {boolean} True if the given |entry| exists in the array. False
380    *     otherwise.
381    */
382   exists: function(entry) {
383     var index = this.getIndex(entry);
384     return (index >= 0);
385   },
386
387   /**
388    * Saves the current array to chrome.storage.
389    * @private
390    */
391   save_: function() {
392     this.rememberLastDriveURL_();
393     if (!this.lastDriveRootURL_)
394       return;
395
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_));
402
403     var prefs = {};
404     prefs[FolderShortcutsDataModel.NAME] = paths;
405     chrome.storage.sync.set(prefs, function() {});
406   },
407
408   /**
409    * Creates a permutation array for 'permuted' event, which is compatible with
410    * a permutation array used in cr/ui/array_data_model.js.
411    *
412    * @param {array} oldArray Previous array before changing.
413    * @param {array} newArray New array after changing.
414    * @return {Array.<number>} Created permutation array.
415    * @private
416    */
417   calculatePermutation_: function(oldArray, newArray) {
418     var oldIndex = 0;  // Index of oldArray.
419     var newIndex = 0;  // Index of newArray.
420
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;
427         continue;
428       }
429
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;
435           newIndex++;
436           break;
437         }
438
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;
442           break;
443         }
444
445         // In the case of this.compare(oldArray[oldIndex]) > 0:
446         // newArray[newIndex] is added, which is not in the old array.
447         newIndex++;
448       }
449     }
450     return permutation;
451   },
452
453   /**
454    * Fires a 'permuted' event, which is compatible with cr.ui.ArrayDataModel.
455    * @param {Array.<number>} permutation Permutation array.
456    */
457   firePermutedEvent_: function(permutation) {
458     var permutedEvent = new Event('permuted');
459     permutedEvent.newLength = this.length;
460     permutedEvent.permutation = permutation;
461     this.dispatchEvent(permutedEvent);
462
463     // Note: This model only fires 'permuted' event, because:
464     // 1) 'change' event is not necessary to fire since it is covered by
465     //    'permuted' event.
466     // 2) 'splice' and 'sorted' events are not implemented. These events are
467     //    not used in NavigationListModel. We have to implement them when
468     //    necessary.
469   },
470
471   /**
472    * Called externally when one of the items is not found on the filesystem.
473    * @param {Entry} entry The entry which is not found.
474    */
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;
483     }
484     this.removeInternal_(entry);
485     this.save_();
486   },
487
488   /**
489    * Converts the given "stored path" to the URL.
490    *
491    * This conversion is necessary because the shortcuts are not stored with
492    * stored-formatted mount paths for compatibility. See http://crbug.com/336155
493    * for detail.
494    *
495    * @param {string} path Path in Drive with the stored drive mount path.
496    * @return {string} URL of the given path.
497    * @private
498    */
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.');
502       return null;
503     }
504     return this.lastDriveRootURL_ + encodeURIComponent(
505         path.substr(STORED_DRIVE_MOUNT_PATH.length));
506   },
507
508   /**
509    * Converts the URL to the stored-formatted path.
510    *
511    * See the comment of convertStoredPathToUrl_() for further information.
512    *
513    * @param {string} url URL of the directory in Drive.
514    * @return {string} Path with the stored drive mount path.
515    * @private
516    */
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.');
521       return null;
522     }
523
524     return STORED_DRIVE_MOUNT_PATH + '/' + decodeURIComponent(
525         url.substr(this.lastDriveRootURL_.length));
526   },
527 };