- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / print_preview / data / destination_store.js
1 // Copyright (c) 2012 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 cr.define('print_preview', function() {
6   'use strict';
7
8   /**
9    * A data store that stores destinations and dispatches events when the data
10    * store changes.
11    * @param {!print_preview.NativeLayer} nativeLayer Used to fetch local print
12    *     destinations.
13    * @param {!print_preview.AppState} appState Application state.
14    * @constructor
15    * @extends {cr.EventTarget}
16    */
17   function DestinationStore(nativeLayer, appState) {
18     cr.EventTarget.call(this);
19
20     /**
21      * Used to fetch local print destinations.
22      * @type {!print_preview.NativeLayer}
23      * @private
24      */
25     this.nativeLayer_ = nativeLayer;
26
27     /**
28      * Used to load and persist the selected destination.
29      * @type {!print_preview.AppState}
30      * @private
31      */
32     this.appState_ = appState;
33
34     /**
35      * Internal backing store for the data store.
36      * @type {!Array.<!print_preview.Destination>}
37      * @private
38      */
39     this.destinations_ = [];
40
41     /**
42      * Cache used for constant lookup of destinations by origin and id.
43      * @type {object.<string, !print_preview.Destination>}
44      * @private
45      */
46     this.destinationMap_ = {};
47
48     /**
49      * Currently selected destination.
50      * @type {print_preview.Destination}
51      * @private
52      */
53     this.selectedDestination_ = null;
54
55     /**
56      * Initial destination ID used to auto-select the first inserted destination
57      * that matches. If {@code null}, the first destination inserted into the
58      * store will be selected.
59      * @type {?string}
60      * @private
61      */
62     this.initialDestinationId_ = null;
63
64     /**
65      * Initial origin used to auto-select destination.
66      * @type {print_preview.Destination.Origin}
67      * @private
68      */
69     this.initialDestinationOrigin_ = print_preview.Destination.Origin.LOCAL;
70
71     /**
72      * Whether the destination store will auto select the destination that
73      * matches the initial destination.
74      * @type {boolean}
75      * @private
76      */
77     this.isInAutoSelectMode_ = false;
78
79     /**
80      * Event tracker used to track event listeners of the destination store.
81      * @type {!EventTracker}
82      * @private
83      */
84     this.tracker_ = new EventTracker();
85
86     /**
87      * Used to fetch cloud-based print destinations.
88      * @type {print_preview.CloudPrintInterface}
89      * @private
90      */
91     this.cloudPrintInterface_ = null;
92
93     /**
94      * Whether the destination store has already loaded or is loading all cloud
95      * destinations.
96      * @type {boolean}
97      * @private
98      */
99     this.hasLoadedAllCloudDestinations_ = false;
100
101     /**
102      * ID of a timeout after the initial destination ID is set. If no inserted
103      * destination matches the initial destination ID after the specified
104      * timeout, the first destination in the store will be automatically
105      * selected.
106      * @type {?number}
107      * @private
108      */
109     this.autoSelectTimeout_ = null;
110
111     /**
112      * Whether a search for local destinations is in progress.
113      * @type {boolean}
114      * @private
115      */
116     this.isLocalDestinationSearchInProgress_ = false;
117
118     this.addEventListeners_();
119     this.reset_();
120   };
121
122   /**
123    * Event types dispatched by the data store.
124    * @enum {string}
125    */
126   DestinationStore.EventType = {
127     DESTINATION_SEARCH_DONE:
128         'print_preview.DestinationStore.DESTINATION_SEARCH_DONE',
129     DESTINATION_SEARCH_STARTED:
130         'print_preview.DestinationStore.DESTINATION_SEARCH_STARTED',
131     DESTINATION_SELECT: 'print_preview.DestinationStore.DESTINATION_SELECT',
132     DESTINATIONS_INSERTED:
133         'print_preview.DestinationStore.DESTINATIONS_INSERTED',
134     SELECTED_DESTINATION_CAPABILITIES_READY:
135         'print_preview.DestinationStore.SELECTED_DESTINATION_CAPABILITIES_READY'
136   };
137
138   /**
139    * Delay in milliseconds before the destination store ignores the initial
140    * destination ID and just selects any printer (since the initial destination
141    * was not found).
142    * @type {number}
143    * @const
144    * @private
145    */
146   DestinationStore.AUTO_SELECT_TIMEOUT_ = 15000;
147
148   /**
149    * Creates a local PDF print destination.
150    * @return {!print_preview.Destination} Created print destination.
151    * @private
152    */
153   DestinationStore.createLocalPdfPrintDestination_ = function() {
154     var dest = new print_preview.Destination(
155         print_preview.Destination.GooglePromotedId.SAVE_AS_PDF,
156         print_preview.Destination.Type.LOCAL,
157         print_preview.Destination.Origin.LOCAL,
158         localStrings.getString('printToPDF'),
159         false /*isRecent*/,
160         print_preview.Destination.ConnectionStatus.ONLINE);
161     dest.capabilities = {
162       version: '1.0',
163       printer: {
164         page_orientation: {
165           option: [
166             {type: 'AUTO', is_default: true},
167             {type: 'PORTRAIT'},
168             {type: 'LANDSCAPE'}
169           ]
170         },
171         color: { option: [{type: 'STANDARD_COLOR', is_default: true}] }
172       }
173     };
174     return dest;
175   };
176
177   DestinationStore.prototype = {
178     __proto__: cr.EventTarget.prototype,
179
180     /**
181      * @return {!Array.<!print_preview.Destination>} List of destinations in
182      *     the store.
183      */
184     get destinations() {
185       return this.destinations_.slice(0);
186     },
187
188     /**
189      * @return {print_preview.Destination} The currently selected destination or
190      *     {@code null} if none is selected.
191      */
192     get selectedDestination() {
193       return this.selectedDestination_;
194     },
195
196     /**
197      * @return {boolean} Whether a search for local destinations is in progress.
198      */
199     get isLocalDestinationSearchInProgress() {
200       return this.isLocalDestinationSearchInProgress_;
201     },
202
203     /**
204      * @return {boolean} Whether a search for cloud destinations is in progress.
205      */
206     get isCloudDestinationSearchInProgress() {
207       return this.cloudPrintInterface_ &&
208              this.cloudPrintInterface_.isCloudDestinationSearchInProgress;
209     },
210
211     /**
212      * Initializes the destination store. Sets the initially selected
213      * destination. If any inserted destinations match this ID, that destination
214      * will be automatically selected. This method must be called after the
215      * print_preview.AppState has been initialized.
216      * @param {?string} systemDefaultDestinationId ID of the system default
217      *     destination.
218      * @private
219      */
220     init: function(systemDefaultDestinationId) {
221       if (this.appState_.selectedDestinationId &&
222           this.appState_.selectedDestinationOrigin) {
223         this.initialDestinationId_ = this.appState_.selectedDestinationId;
224         this.initialDestinationOrigin_ =
225             this.appState_.selectedDestinationOrigin;
226       } else {
227         this.initialDestinationId_ = systemDefaultDestinationId;
228         this.initialDestinationOrigin_ = print_preview.Destination.Origin.LOCAL;
229       }
230       this.isInAutoSelectMode_ = true;
231       if (this.initialDestinationId_ == null ||
232           this.initialDestinationOrigin_ == null) {
233         assert(this.destinations_.length > 0,
234                'No destinations available to select');
235         this.selectDestination(this.destinations_[0]);
236       } else {
237         var key = this.getDestinationKey_(this.initialDestinationOrigin_,
238                                           this.initialDestinationId_);
239         var candidate = this.destinationMap_[key];
240         if (candidate != null) {
241           this.selectDestination(candidate);
242         } else if (!cr.isChromeOS &&
243                    this.initialDestinationOrigin_ ==
244                    print_preview.Destination.Origin.LOCAL) {
245           this.nativeLayer_.startGetLocalDestinationCapabilities(
246               this.initialDestinationId_);
247         }
248       }
249     },
250
251     /**
252      * Sets the destination store's Google Cloud Print interface.
253      * @param {!print_preview.CloudPrintInterface} cloudPrintInterface Interface
254      *     to set.
255      */
256     setCloudPrintInterface: function(cloudPrintInterface) {
257       this.cloudPrintInterface_ = cloudPrintInterface;
258       this.tracker_.add(
259           this.cloudPrintInterface_,
260           cloudprint.CloudPrintInterface.EventType.SEARCH_DONE,
261           this.onCloudPrintSearchDone_.bind(this));
262       this.tracker_.add(
263           this.cloudPrintInterface_,
264           cloudprint.CloudPrintInterface.EventType.SEARCH_FAILED,
265           this.onCloudPrintSearchFailed_.bind(this));
266       this.tracker_.add(
267           this.cloudPrintInterface_,
268           cloudprint.CloudPrintInterface.EventType.PRINTER_DONE,
269           this.onCloudPrintPrinterDone_.bind(this));
270       this.tracker_.add(
271           this.cloudPrintInterface_,
272           cloudprint.CloudPrintInterface.EventType.PRINTER_FAILED,
273           this.onCloudPrintPrinterFailed_.bind(this));
274       // Fetch initial destination if its a cloud destination.
275       var origin = this.initialDestinationOrigin_;
276       if (this.isInAutoSelectMode_ &&
277           origin != print_preview.Destination.Origin.LOCAL) {
278         this.cloudPrintInterface_.printer(this.initialDestinationId_, origin);
279       }
280     },
281
282     /**
283      * @return {boolean} Whether only default cloud destinations have been
284      *     loaded.
285      */
286     hasOnlyDefaultCloudDestinations: function() {
287       return this.destinations_.every(function(dest) {
288         return dest.isLocal ||
289             dest.id == print_preview.Destination.GooglePromotedId.DOCS ||
290             dest.id == print_preview.Destination.GooglePromotedId.FEDEX;
291       });
292     },
293
294     /** @param {!print_preview.Destination} Destination to select. */
295     selectDestination: function(destination) {
296       this.selectedDestination_ = destination;
297       this.selectedDestination_.isRecent = true;
298       this.isInAutoSelectMode_ = false;
299       if (this.autoSelectTimeout_ != null) {
300         clearTimeout(this.autoSelectTimeout_);
301         this.autoSelectTimeout_ = null;
302       }
303       if (destination.id == print_preview.Destination.GooglePromotedId.FEDEX &&
304           !destination.isTosAccepted) {
305         assert(this.cloudPrintInterface_ != null,
306                'Selected FedEx Office destination, but Google Cloud Print is ' +
307                'not enabled');
308         destination.isTosAccepted = true;
309         this.cloudPrintInterface_.updatePrinterTosAcceptance(destination.id,
310                                                              destination.origin,
311                                                              true);
312       }
313       this.appState_.persistSelectedDestination(this.selectedDestination_);
314       cr.dispatchSimpleEvent(
315           this, DestinationStore.EventType.DESTINATION_SELECT);
316       if (destination.capabilities == null) {
317          if (destination.isLocal) {
318           this.nativeLayer_.startGetLocalDestinationCapabilities(
319               destination.id);
320         } else {
321           assert(this.cloudPrintInterface_ != null,
322                  'Selected destination is a cloud destination, but Google ' +
323                  'Cloud Print is not enabled');
324           this.cloudPrintInterface_.printer(destination.id,
325                                             destination.origin);
326         }
327       } else {
328         cr.dispatchSimpleEvent(
329             this,
330             DestinationStore.EventType.SELECTED_DESTINATION_CAPABILITIES_READY);
331       }
332     },
333
334     /**
335      * Inserts a print destination to the data store and dispatches a
336      * DESTINATIONS_INSERTED event. If the destination matches the initial
337      * destination ID, then the destination will be automatically selected.
338      * @param {!print_preview.Destination} destination Print destination to
339      *     insert.
340      */
341     insertDestination: function(destination) {
342       if (this.insertDestination_(destination)) {
343         cr.dispatchSimpleEvent(
344             this, DestinationStore.EventType.DESTINATIONS_INSERTED);
345         if (this.isInAutoSelectMode_ &&
346             this.matchInitialDestination_(destination.id, destination.origin)) {
347           this.selectDestination(destination);
348         }
349       }
350     },
351
352     /**
353      * Inserts multiple print destinations to the data store and dispatches one
354      * DESTINATIONS_INSERTED event. If any of the destinations match the initial
355      * destination ID, then that destination will be automatically selected.
356      * @param {!Array.<print_preview.Destination>} destinations Print
357      *     destinations to insert.
358      */
359     insertDestinations: function(destinations) {
360       var insertedDestination = false;
361       var destinationToAutoSelect = null;
362       destinations.forEach(function(dest) {
363         if (this.insertDestination_(dest)) {
364           insertedDestination = true;
365           if (this.isInAutoSelectMode_ &&
366               destinationToAutoSelect == null &&
367               this.matchInitialDestination_(dest.id, dest.origin)) {
368             destinationToAutoSelect = dest;
369           }
370         }
371       }, this);
372       if (insertedDestination) {
373         cr.dispatchSimpleEvent(
374             this, DestinationStore.EventType.DESTINATIONS_INSERTED);
375       }
376       if (destinationToAutoSelect != null) {
377         this.selectDestination(destinationToAutoSelect);
378       }
379     },
380
381     /**
382      * Updates an existing print destination with capabilities information. If
383      * the destination doesn't already exist, it will be added.
384      * @param {!print_preview.Destination} destination Destination to update.
385      * @return {!print_preview.Destination} The existing destination that was
386      *     updated or {@code null} if it was the new destination.
387      */
388     updateDestination: function(destination) {
389       var key = this.getDestinationKey_(destination.origin, destination.id);
390       var existingDestination = this.destinationMap_[key];
391       if (existingDestination != null) {
392         existingDestination.capabilities = destination.capabilities;
393         return existingDestination;
394       } else {
395         this.insertDestination(destination);
396         return null;
397       }
398     },
399
400     /** Initiates loading of local print destinations. */
401     startLoadLocalDestinations: function() {
402       this.nativeLayer_.startGetLocalDestinations();
403       this.isLocalDestinationSearchInProgress_ = true;
404       cr.dispatchSimpleEvent(
405           this, DestinationStore.EventType.DESTINATION_SEARCH_STARTED);
406     },
407
408     /**
409      * Initiates loading of cloud destinations.
410      * @param {boolean} recentOnly Whether the load recet destinations only.
411      */
412     startLoadCloudDestinations: function(recentOnly) {
413       if (this.cloudPrintInterface_ != null &&
414           !this.hasLoadedAllCloudDestinations_ &&
415           (!recentOnly || !this.isCloudDestinationSearchInProgress)) {
416         this.cloudPrintInterface_.search(recentOnly);
417         this.hasLoadedAllCloudDestinations_ = !recentOnly;
418         cr.dispatchSimpleEvent(
419             this, DestinationStore.EventType.DESTINATION_SEARCH_STARTED);
420       }
421     },
422
423     /**
424      * Inserts a destination into the store without dispatching any events.
425      * @return {boolean} Whether the inserted destination was not already in the
426      *     store.
427      * @private
428      */
429     insertDestination_: function(destination) {
430       var key = this.getDestinationKey_(destination.origin, destination.id);
431       var existingDestination = this.destinationMap_[key];
432       if (existingDestination == null) {
433         this.destinations_.push(destination);
434         this.destinationMap_[key] = destination;
435         return true;
436       } else if (existingDestination.connectionStatus ==
437                      print_preview.Destination.ConnectionStatus.UNKNOWN &&
438                  destination.connectionStatus !=
439                      print_preview.Destination.ConnectionStatus.UNKNOWN) {
440         existingDestination.connectionStatus = destination.connectionStatus;
441         return true;
442       } else {
443         return false;
444       }
445     },
446
447     /**
448      * Binds handlers to events.
449      * @private
450      */
451     addEventListeners_: function() {
452       this.tracker_.add(
453           this.nativeLayer_,
454           print_preview.NativeLayer.EventType.LOCAL_DESTINATIONS_SET,
455           this.onLocalDestinationsSet_.bind(this));
456       this.tracker_.add(
457           this.nativeLayer_,
458           print_preview.NativeLayer.EventType.CAPABILITIES_SET,
459           this.onLocalDestinationCapabilitiesSet_.bind(this));
460       this.tracker_.add(
461           this.nativeLayer_,
462           print_preview.NativeLayer.EventType.GET_CAPABILITIES_FAIL,
463           this.onGetCapabilitiesFail_.bind(this));
464       this.tracker_.add(
465           this.nativeLayer_,
466           print_preview.NativeLayer.EventType.DESTINATIONS_RELOAD,
467           this.onDestinationsReload_.bind(this));
468     },
469
470     /**
471      * Resets the state of the destination store to its initial state.
472      * @private
473      */
474     reset_: function() {
475       this.destinations_ = [];
476       this.destinationMap_ = {};
477       this.selectedDestination_ = null;
478       this.hasLoadedAllCloudDestinations_ = false;
479       this.insertDestination(
480           DestinationStore.createLocalPdfPrintDestination_());
481       this.autoSelectTimeout_ = setTimeout(
482           this.onAutoSelectTimeoutExpired_.bind(this),
483           DestinationStore.AUTO_SELECT_TIMEOUT_);
484     },
485
486     /**
487      * Called when the local destinations have been got from the native layer.
488      * @param {Event} Contains the local destinations.
489      * @private
490      */
491     onLocalDestinationsSet_: function(event) {
492       var localDestinations = event.destinationInfos.map(function(destInfo) {
493         return print_preview.LocalDestinationParser.parse(destInfo);
494       });
495       this.insertDestinations(localDestinations);
496       this.isLocalDestinationSearchInProgress_ = false;
497       cr.dispatchSimpleEvent(
498           this, DestinationStore.EventType.DESTINATION_SEARCH_DONE);
499     },
500
501     /**
502      * Called when the native layer retrieves the capabilities for the selected
503      * local destination. Updates the destination with new capabilities if the
504      * destination already exists, otherwise it creates a new destination and
505      * then updates its capabilities.
506      * @param {Event} event Contains the capabilities of the local print
507      *     destination.
508      * @private
509      */
510     onLocalDestinationCapabilitiesSet_: function(event) {
511       var destinationId = event.settingsInfo['printerId'];
512       var key =
513           this.getDestinationKey_(print_preview.Destination.Origin.LOCAL,
514                                   destinationId);
515       var destination = this.destinationMap_[key];
516       var capabilities = print_preview.LocalCapabilitiesParser.parse(
517             event.settingsInfo);
518       if (destination) {
519         // In case there were multiple capabilities request for this local
520         // destination, just ignore the later ones.
521         if (destination.capabilities != null) {
522           return;
523         }
524         destination.capabilities = capabilities;
525       } else {
526         // TODO(rltoscano): This makes the assumption that the "deviceName" is
527         // the same as "printerName". We should include the "printerName" in the
528         // response. See http://crbug.com/132831.
529         destination = print_preview.LocalDestinationParser.parse(
530             {deviceName: destinationId, printerName: destinationId});
531         destination.capabilities = capabilities;
532         this.insertDestination(destination);
533       }
534       if (this.selectedDestination_ &&
535           this.selectedDestination_.id == destinationId) {
536         cr.dispatchSimpleEvent(this,
537                                DestinationStore.EventType.
538                                    SELECTED_DESTINATION_CAPABILITIES_READY);
539       }
540     },
541
542     /**
543      * Called when a request to get a local destination's print capabilities
544      * fails. If the destination is the initial destination, auto-select another
545      * destination instead.
546      * @param {Event} event Contains the destination ID that failed.
547      * @private
548      */
549     onGetCapabilitiesFail_: function(event) {
550       console.error('Failed to get print capabilities for printer ' +
551                     event.destinationId);
552       if (this.isInAutoSelectMode_ &&
553           this.matchInitialDestinationStrict_(event.destinationId,
554                                               event.destinationOrigin)) {
555         assert(this.destinations_.length > 0,
556                'No destinations were loaded when failed to get initial ' +
557                'destination');
558         this.selectDestination(this.destinations_[0]);
559       }
560     },
561
562     /**
563      * Called when the /search call completes. Adds the fetched destinations to
564      * the destination store.
565      * @param {Event} event Contains the fetched destinations.
566      * @private
567      */
568     onCloudPrintSearchDone_: function(event) {
569       this.insertDestinations(event.printers);
570       cr.dispatchSimpleEvent(
571           this, DestinationStore.EventType.DESTINATION_SEARCH_DONE);
572     },
573
574     /**
575      * Called when the /search call fails. Updates outstanding request count and
576      * dispatches CLOUD_DESTINATIONS_LOADED event.
577      * @private
578      */
579     onCloudPrintSearchFailed_: function() {
580       cr.dispatchSimpleEvent(
581           this, DestinationStore.EventType.DESTINATION_SEARCH_DONE);
582     },
583
584     /**
585      * Called when /printer call completes. Updates the specified destination's
586      * print capabilities.
587      * @param {Event} event Contains detailed information about the
588      *     destination.
589      * @private
590      */
591     onCloudPrintPrinterDone_: function(event) {
592       var dest = this.updateDestination(event.printer);
593       if (this.selectedDestination_ == dest) {
594         cr.dispatchSimpleEvent(
595             this,
596             DestinationStore.EventType.SELECTED_DESTINATION_CAPABILITIES_READY);
597       }
598     },
599
600     /**
601      * Called when the Google Cloud Print interface fails to lookup a
602      * destination. Selects another destination if the failed destination was
603      * the initial destination.
604      * @param {object} event Contains the ID of the destination that was failed
605      *     to be looked up.
606      * @private
607      */
608     onCloudPrintPrinterFailed_: function(event) {
609       if (this.isInAutoSelectMode_ &&
610           this.matchInitialDestinationStrict_(event.destinationId,
611                                               event.destinationOrigin)) {
612         console.error('Could not find initial printer: ' + event.destinationId);
613         assert(this.destinations_.length > 0,
614                'No destinations were loaded when failed to get initial ' +
615                'destination');
616         this.selectDestination(this.destinations_[0]);
617       }
618     },
619
620     /**
621      * Called from native layer after the user was requested to sign in, and did
622      * so successfully.
623      * @private
624      */
625     onDestinationsReload_: function() {
626       this.reset_();
627       this.isInAutoSelectMode_ = true;
628       this.startLoadLocalDestinations();
629       this.startLoadCloudDestinations(true);
630       this.startLoadCloudDestinations(false);
631     },
632
633     /**
634      * Called when no destination was auto-selected after some timeout. Selects
635      * the first destination in store.
636      * @private
637      */
638     onAutoSelectTimeoutExpired_: function() {
639       this.autoSelectTimeout_ = null;
640       assert(this.destinations_.length > 0,
641              'No destinations were loaded before auto-select timeout expired');
642       this.selectDestination(this.destinations_[0]);
643     },
644
645     // TODO(vitalybuka): Remove three next functions replacing Destination.id
646     //    and Destination.origin by complex ID.
647     /**
648      * Returns key to be used with {@code destinationMap_}.
649      * @param {!print_preview.Destination.Origin} origin Destination origin.
650      * @return {!string} id Destination id.
651      * @private
652      */
653     getDestinationKey_: function(origin, id) {
654       return origin + '/' + id;
655     },
656
657     /**
658      * @param {?string} id Id of the destination.
659      * @param {?string} origin Oring of the destination.
660      * @return {boolean} Whether a initial destination matches provided.
661      * @private
662      */
663     matchInitialDestination_: function(id, origin) {
664       return this.initialDestinationId_ == null ||
665              this.initialDestinationOrigin_ == null ||
666              this.matchInitialDestinationStrict_(id, origin);
667     },
668
669     /**
670      * @param {?string} id Id of the destination.
671      * @param {?string} origin Oring of the destination.
672      * @return {boolean} Whether destination is the same as initial.
673      * @private
674      */
675     matchInitialDestinationStrict_: function(id, origin) {
676       return id == this.initialDestinationId_ &&
677              origin == this.initialDestinationOrigin_;
678     }
679   };
680
681   // Export
682   return {
683     DestinationStore: DestinationStore
684   };
685 });