Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / print_preview / data / destination_store.js
blob5aeaffe2c337573ac036b88697a3577df4fc51b5
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.
5 cr.define('print_preview', function() {
6   'use strict';
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.UserInfo} userInfo User information repository.
14    * @param {!print_preview.AppState} appState Application state.
15    * @constructor
16    * @extends {cr.EventTarget}
17    */
18   function DestinationStore(nativeLayer, userInfo, appState) {
19     cr.EventTarget.call(this);
21     /**
22      * Used to fetch local print destinations.
23      * @type {!print_preview.NativeLayer}
24      * @private
25      */
26     this.nativeLayer_ = nativeLayer;
28     /**
29      * User information repository.
30      * @type {!print_preview.UserInfo}
31      * @private
32      */
33     this.userInfo_ = userInfo;
35     /**
36      * Used to load and persist the selected destination.
37      * @type {!print_preview.AppState}
38      * @private
39      */
40     this.appState_ = appState;
42     /**
43      * Used to track metrics.
44      * @type {!print_preview.DestinationSearchMetricsContext}
45      * @private
46      */
47     this.metrics_ = new print_preview.DestinationSearchMetricsContext();
49     /**
50      * Internal backing store for the data store.
51      * @type {!Array<!print_preview.Destination>}
52      * @private
53      */
54     this.destinations_ = [];
56     /**
57      * Cache used for constant lookup of destinations by origin and id.
58      * @type {Object<!print_preview.Destination>}
59      * @private
60      */
61     this.destinationMap_ = {};
63     /**
64      * Currently selected destination.
65      * @type {print_preview.Destination}
66      * @private
67      */
68     this.selectedDestination_ = null;
70     /**
71      * Whether the destination store will auto select the destination that
72      * matches the last used destination stored in appState_.
73      * @type {boolean}
74      * @private
75      */
76     this.isInAutoSelectMode_ = false;
78     /**
79      * Event tracker used to track event listeners of the destination store.
80      * @type {!EventTracker}
81      * @private
82      */
83     this.tracker_ = new EventTracker();
85     /**
86      * Whether PDF printer is enabled. It's disabled, for example, in App Kiosk
87      * mode.
88      * @type {boolean}
89      * @private
90      */
91     this.pdfPrinterEnabled_ = false;
93     /**
94      * Used to fetch cloud-based print destinations.
95      * @type {cloudprint.CloudPrintInterface}
96      * @private
97      */
98     this.cloudPrintInterface_ = null;
100     /**
101      * Maps user account to the list of origins for which destinations are
102      * already loaded.
103      * @type {!Object<Array<print_preview.Destination.Origin>>}
104      * @private
105      */
106     this.loadedCloudOrigins_ = {};
108     /**
109      * ID of a timeout after the initial destination ID is set. If no inserted
110      * destination matches the initial destination ID after the specified
111      * timeout, the first destination in the store will be automatically
112      * selected.
113      * @type {?number}
114      * @private
115      */
116     this.autoSelectTimeout_ = null;
118     /**
119      * Whether a search for local destinations is in progress.
120      * @type {boolean}
121      * @private
122      */
123     this.isLocalDestinationSearchInProgress_ = false;
125     /**
126      * Whether the destination store has already loaded or is loading all local
127      * destinations.
128      * @type {boolean}
129      * @private
130      */
131     this.hasLoadedAllLocalDestinations_ = false;
133     /**
134      * Whether a search for privet destinations is in progress.
135      * @type {boolean}
136      * @private
137      */
138     this.isPrivetDestinationSearchInProgress_ = false;
140     /**
141      * Whether the destination store has already loaded or is loading all privet
142      * destinations.
143      * @type {boolean}
144      * @private
145      */
146     this.hasLoadedAllPrivetDestinations_ = false;
148     /**
149      * ID of a timeout after the start of a privet search to end that privet
150      * search.
151      * @type {?number}
152      * @private
153      */
154     this.privetSearchTimeout_ = null;
156     /**
157      * Whether a search for extension destinations is in progress.
158      * @type {boolean}
159      * @private
160      */
161     this.isExtensionDestinationSearchInProgress_ = false;
163     /**
164      * Whether the destination store has already loaded all extension
165      * destinations.
166      * @type {boolean}
167      * @private
168      */
169     this.hasLoadedAllExtensionDestinations_ = false;
171     /**
172      * ID of a timeout set at the start of an extension destination search. The
173      * timeout ends the search.
174      * @type {?number}
175      * @private
176      */
177     this.extensionSearchTimeout_ = null;
179     /**
180      * MDNS service name of destination that we are waiting to register.
181      * @type {?string}
182      * @private
183      */
184     this.waitForRegisterDestination_ = null;
186     this.addEventListeners_();
187     this.reset_();
188   };
190   /**
191    * Event types dispatched by the data store.
192    * @enum {string}
193    */
194   DestinationStore.EventType = {
195     DESTINATION_SEARCH_DONE:
196         'print_preview.DestinationStore.DESTINATION_SEARCH_DONE',
197     DESTINATION_SEARCH_STARTED:
198         'print_preview.DestinationStore.DESTINATION_SEARCH_STARTED',
199     DESTINATION_SELECT: 'print_preview.DestinationStore.DESTINATION_SELECT',
200     DESTINATIONS_INSERTED:
201         'print_preview.DestinationStore.DESTINATIONS_INSERTED',
202     PROVISIONAL_DESTINATION_RESOLVED:
203         'print_preview.DestinationStore.PROVISIONAL_DESTINATION_RESOLVED',
204     CACHED_SELECTED_DESTINATION_INFO_READY:
205         'print_preview.DestinationStore.CACHED_SELECTED_DESTINATION_INFO_READY',
206     SELECTED_DESTINATION_CAPABILITIES_READY:
207         'print_preview.DestinationStore.SELECTED_DESTINATION_CAPABILITIES_READY'
208   };
210   /**
211    * Delay in milliseconds before the destination store ignores the initial
212    * destination ID and just selects any printer (since the initial destination
213    * was not found).
214    * @type {number}
215    * @const
216    * @private
217    */
218   DestinationStore.AUTO_SELECT_TIMEOUT_ = 15000;
220   /**
221    * Amount of time spent searching for privet destination, in milliseconds.
222    * @type {number}
223    * @const
224    * @private
225    */
226   DestinationStore.PRIVET_SEARCH_DURATION_ = 5000;
228   /**
229    * Maximum amount of time spent searching for extension destinations, in
230    * milliseconds.
231    * @type {number}
232    * @const
233    * @private
234    */
235   DestinationStore.EXTENSION_SEARCH_DURATION_ = 5000;
237   /**
238    * Localizes printer capabilities.
239    * @param {!Object} capabilities Printer capabilities to localize.
240    * @return {!Object} Localized capabilities.
241    * @private
242    */
243   DestinationStore.localizeCapabilities_ = function(capabilities) {
244     var mediaSize = capabilities.printer.media_size;
245     if (mediaSize) {
246       var mediaDisplayNames = {
247         'ISO_A4': 'A4',
248         'ISO_A3': 'A3',
249         'NA_LETTER': 'Letter',
250         'NA_LEGAL': 'Legal',
251         'NA_LEDGER': 'Tabloid'
252       };
253       for (var i = 0, media; media = mediaSize.option[i]; i++) {
254         // No need to patch capabilities with localized names provided.
255         if (!media.custom_display_name_localized) {
256           media.custom_display_name =
257               media.custom_display_name ||
258               mediaDisplayNames[media.name] ||
259               media.name;
260         }
261       }
262     }
263     return capabilities;
264   };
266   DestinationStore.prototype = {
267     __proto__: cr.EventTarget.prototype,
269     /**
270      * @param {string=} opt_account Account to filter destinations by. When
271      *     omitted, all destinations are returned.
272      * @return {!Array<!print_preview.Destination>} List of destinations
273      *     accessible by the {@code account}.
274      */
275     destinations: function(opt_account) {
276       if (opt_account) {
277         return this.destinations_.filter(function(destination) {
278           return !destination.account || destination.account == opt_account;
279         });
280       } else {
281         return this.destinations_.slice(0);
282       }
283     },
285     /**
286      * @return {print_preview.Destination} The currently selected destination or
287      *     {@code null} if none is selected.
288      */
289     get selectedDestination() {
290       return this.selectedDestination_;
291     },
293     /** @return {boolean} Whether destination selection is pending or not. */
294     get isAutoSelectDestinationInProgress() {
295       return this.selectedDestination_ == null &&
296           this.autoSelectTimeout_ != null;
297     },
299     /**
300      * @return {boolean} Whether a search for local destinations is in progress.
301      */
302     get isLocalDestinationSearchInProgress() {
303       return this.isLocalDestinationSearchInProgress_ ||
304         this.isPrivetDestinationSearchInProgress_ ||
305         this.isExtensionDestinationSearchInProgress_;
306     },
308     /**
309      * @return {boolean} Whether a search for cloud destinations is in progress.
310      */
311     get isCloudDestinationSearchInProgress() {
312       return !!this.cloudPrintInterface_ &&
313              this.cloudPrintInterface_.isCloudDestinationSearchInProgress;
314     },
316     /**
317      * Initializes the destination store. Sets the initially selected
318      * destination. If any inserted destinations match this ID, that destination
319      * will be automatically selected. This method must be called after the
320      * print_preview.AppState has been initialized.
321      * @param {boolean} isInAppKioskMode Whether the print preview is in App
322      *     Kiosk mode.
323      */
324     init: function(isInAppKioskMode) {
325       this.pdfPrinterEnabled_ = !isInAppKioskMode;
326       this.isInAutoSelectMode_ = true;
327       this.createLocalPdfPrintDestination_();
328       if (!this.appState_.selectedDestinationId ||
329           !this.appState_.selectedDestinationOrigin) {
330         this.selectDefaultDestination_();
331       } else {
332         var key = this.getDestinationKey_(
333             this.appState_.selectedDestinationOrigin,
334             this.appState_.selectedDestinationId,
335             this.appState_.selectedDestinationAccount || '');
336         var candidate = this.destinationMap_[key];
337         if (candidate != null) {
338           this.selectDestination(candidate);
339         } else if (this.appState_.selectedDestinationOrigin ==
340                    print_preview.Destination.Origin.LOCAL) {
341           this.nativeLayer_.startGetLocalDestinationCapabilities(
342               this.appState_.selectedDestinationId);
343         } else if (this.cloudPrintInterface_ &&
344                    (this.appState_.selectedDestinationOrigin ==
345                         print_preview.Destination.Origin.COOKIES ||
346                     this.appState_.selectedDestinationOrigin ==
347                         print_preview.Destination.Origin.DEVICE)) {
348           this.cloudPrintInterface_.printer(
349               this.appState_.selectedDestinationId,
350               this.appState_.selectedDestinationOrigin,
351               this.appState_.selectedDestinationAccount || '');
352         } else if (this.appState_.selectedDestinationOrigin ==
353                    print_preview.Destination.Origin.PRIVET) {
354           // TODO(noamsml): Resolve a specific printer instead of listing all
355           // privet printers in this case.
356           this.nativeLayer_.startGetPrivetDestinations();
358           var destinationName = this.appState_.selectedDestinationName || '';
360           // Create a fake selectedDestination_ that is not actually in the
361           // destination store. When the real destination is created, this
362           // destination will be overwritten.
363           this.selectedDestination_ = new print_preview.Destination(
364               this.appState_.selectedDestinationId,
365               print_preview.Destination.Type.LOCAL,
366               print_preview.Destination.Origin.PRIVET,
367               destinationName,
368               false /*isRecent*/,
369               print_preview.Destination.ConnectionStatus.ONLINE);
370           this.selectedDestination_.capabilities =
371               this.appState_.selectedDestinationCapabilities;
373           cr.dispatchSimpleEvent(
374             this,
375             DestinationStore.EventType.CACHED_SELECTED_DESTINATION_INFO_READY);
376         } else if (this.appState_.selectedDestinationOrigin ==
377                    print_preview.Destination.Origin.EXTENSION) {
378           // TODO(tbarzic): Add support for requesting a single extension's
379           // printer list.
380           this.startLoadExtensionDestinations();
382           this.selectedDestination_ =
383               print_preview.ExtensionDestinationParser.parse({
384                 extensionId: this.appState_.selectedDestinationExtensionId,
385                 extensionName: this.appState_.selectedDestinationExtensionName,
386                 id: this.appState_.selectedDestinationId,
387                 name: this.appState_.selectedDestinationName || ''
388               });
390           if (this.appState_.selectedDestinationCapabilities) {
391             this.selectedDestination_.capabilities =
392                 this.appState_.selectedDestinationCapabilities;
394             cr.dispatchSimpleEvent(
395                 this,
396                 DestinationStore.EventType
397                     .CACHED_SELECTED_DESTINATION_INFO_READY);
398           }
399         } else {
400           this.selectDefaultDestination_();
401         }
402       }
403     },
405     /**
406      * Sets the destination store's Google Cloud Print interface.
407      * @param {!cloudprint.CloudPrintInterface} cloudPrintInterface Interface
408      *     to set.
409      */
410     setCloudPrintInterface: function(cloudPrintInterface) {
411       this.cloudPrintInterface_ = cloudPrintInterface;
412       this.tracker_.add(
413           this.cloudPrintInterface_,
414           cloudprint.CloudPrintInterface.EventType.SEARCH_DONE,
415           this.onCloudPrintSearchDone_.bind(this));
416       this.tracker_.add(
417           this.cloudPrintInterface_,
418           cloudprint.CloudPrintInterface.EventType.SEARCH_FAILED,
419           this.onCloudPrintSearchDone_.bind(this));
420       this.tracker_.add(
421           this.cloudPrintInterface_,
422           cloudprint.CloudPrintInterface.EventType.PRINTER_DONE,
423           this.onCloudPrintPrinterDone_.bind(this));
424       this.tracker_.add(
425           this.cloudPrintInterface_,
426           cloudprint.CloudPrintInterface.EventType.PRINTER_FAILED,
427           this.onCloudPrintPrinterFailed_.bind(this));
428       this.tracker_.add(
429           this.cloudPrintInterface_,
430           cloudprint.CloudPrintInterface.EventType.PROCESS_INVITE_DONE,
431           this.onCloudPrintProcessInviteDone_.bind(this));
432     },
434     /**
435      * @return {boolean} Whether only default cloud destinations have been
436      *     loaded.
437      */
438     hasOnlyDefaultCloudDestinations: function() {
439       // TODO: Move the logic to print_preview.
440       return this.destinations_.every(function(dest) {
441         return dest.isLocal ||
442             dest.id == print_preview.Destination.GooglePromotedId.DOCS ||
443             dest.id == print_preview.Destination.GooglePromotedId.FEDEX;
444       });
445     },
447     /**
448      * @param {print_preview.Destination} destination Destination to select.
449      */
450     selectDestination: function(destination) {
451       this.isInAutoSelectMode_ = false;
452       // When auto select expires, DESTINATION_SELECT event has to be dispatched
453       // anyway (see isAutoSelectDestinationInProgress() logic).
454       if (this.autoSelectTimeout_) {
455         clearTimeout(this.autoSelectTimeout_);
456         this.autoSelectTimeout_ = null;
457       } else if (destination == this.selectedDestination_) {
458         return;
459       }
460       if (destination == null) {
461         this.selectedDestination_ = null;
462         cr.dispatchSimpleEvent(
463             this, DestinationStore.EventType.DESTINATION_SELECT);
464         return;
465       }
467       assert(!destination.isProvisional,
468              'Unable to select provisonal destinations');
470       // Update and persist selected destination.
471       this.selectedDestination_ = destination;
472       this.selectedDestination_.isRecent = true;
473       if (destination.id == print_preview.Destination.GooglePromotedId.FEDEX &&
474           !destination.isTosAccepted) {
475         assert(this.cloudPrintInterface_ != null,
476                'Selected FedEx destination, but GCP API is not available');
477         destination.isTosAccepted = true;
478         this.cloudPrintInterface_.updatePrinterTosAcceptance(destination, true);
479       }
480       this.appState_.persistSelectedDestination(this.selectedDestination_);
481       // Adjust metrics.
482       if (destination.cloudID &&
483           this.destinations_.some(function(otherDestination) {
484             return otherDestination.cloudID == destination.cloudID &&
485                 otherDestination != destination;
486           })) {
487         this.metrics_.record(destination.isPrivet ?
488             print_preview.Metrics.DestinationSearchBucket.
489                 PRIVET_DUPLICATE_SELECTED :
490             print_preview.Metrics.DestinationSearchBucket.
491                 CLOUD_DUPLICATE_SELECTED);
492       }
493       // Notify about selected destination change.
494       cr.dispatchSimpleEvent(
495           this, DestinationStore.EventType.DESTINATION_SELECT);
496       // Request destination capabilities, of not known yet.
497       if (destination.capabilities == null) {
498         if (destination.isPrivet) {
499           this.nativeLayer_.startGetPrivetDestinationCapabilities(
500               destination.id);
501         } else if (destination.isExtension) {
502           this.nativeLayer_.startGetExtensionDestinationCapabilities(
503               destination.id);
504         } else if (destination.isLocal) {
505           this.nativeLayer_.startGetLocalDestinationCapabilities(
506               destination.id);
507         } else {
508           assert(this.cloudPrintInterface_ != null,
509                  'Cloud destination selected, but GCP is not enabled');
510           this.cloudPrintInterface_.printer(
511               destination.id, destination.origin, destination.account);
512         }
513       } else {
514         cr.dispatchSimpleEvent(
515             this,
516             DestinationStore.EventType.SELECTED_DESTINATION_CAPABILITIES_READY);
517       }
518     },
520     /**
521      * Attempts to resolve a provisional destination.
522      * @param {!print_preview.Destination} destinaion Provisional destination
523      *     that should be resolved.
524      */
525     resolveProvisionalDestination: function(destination) {
526       assert(
527           destination.provisionalType ==
528               print_preview.Destination.ProvisionalType.NEEDS_USB_PERMISSION,
529           'Provisional type cannot be resolved.');
530       this.nativeLayer_.grantExtensionPrinterAccess(destination.id);
531     },
533     /**
534      * Selects 'Save to PDF' destination (since it always exists).
535      * @private
536      */
537     selectDefaultDestination_: function() {
538       var saveToPdfKey = this.getDestinationKey_(
539           print_preview.Destination.Origin.LOCAL,
540           print_preview.Destination.GooglePromotedId.SAVE_AS_PDF,
541           '');
542       this.selectDestination(
543           this.destinationMap_[saveToPdfKey] || this.destinations_[0] || null);
544     },
546     /** Initiates loading of local print destinations. */
547     startLoadLocalDestinations: function() {
548       if (!this.hasLoadedAllLocalDestinations_) {
549         this.hasLoadedAllLocalDestinations_ = true;
550         this.nativeLayer_.startGetLocalDestinations();
551         this.isLocalDestinationSearchInProgress_ = true;
552         cr.dispatchSimpleEvent(
553             this, DestinationStore.EventType.DESTINATION_SEARCH_STARTED);
554       }
555     },
557     /** Initiates loading of privet print destinations. */
558     startLoadPrivetDestinations: function() {
559       if (!this.hasLoadedAllPrivetDestinations_) {
560         if (this.privetDestinationSearchInProgress_)
561           clearTimeout(this.privetSearchTimeout_);
562         this.isPrivetDestinationSearchInProgress_ = true;
563         this.nativeLayer_.startGetPrivetDestinations();
564         cr.dispatchSimpleEvent(
565             this, DestinationStore.EventType.DESTINATION_SEARCH_STARTED);
566         this.privetSearchTimeout_ = setTimeout(
567             this.endPrivetPrinterSearch_.bind(this),
568             DestinationStore.PRIVET_SEARCH_DURATION_);
569       }
570     },
572     /** Initializes loading of extension managed print destinations. */
573     startLoadExtensionDestinations: function() {
574       if (this.hasLoadedAllExtensionDestinations_)
575         return;
577       if (this.isExtensionDestinationSearchInProgress_)
578         clearTimeout(this.extensionSearchTimeout_);
580       this.isExtensionDestinationSearchInProgress_ = true;
581       this.nativeLayer_.startGetExtensionDestinations();
582       cr.dispatchSimpleEvent(
583           this, DestinationStore.EventType.DESTINATION_SEARCH_STARTED);
584       this.extensionSearchTimeout_ = setTimeout(
585           this.endExtensionPrinterSearch_.bind(this),
586           DestinationStore.EXTENSION_SEARCH_DURATION_);
587     },
589     /**
590      * Initiates loading of cloud destinations.
591      * @param {print_preview.Destination.Origin=} opt_origin Search destinations
592      *     for the specified origin only.
593      */
594     startLoadCloudDestinations: function(opt_origin) {
595       if (this.cloudPrintInterface_ != null) {
596         var origins = this.loadedCloudOrigins_[this.userInfo_.activeUser] || [];
597         if (origins.length == 0 ||
598             (opt_origin && origins.indexOf(opt_origin) < 0)) {
599           this.cloudPrintInterface_.search(
600               this.userInfo_.activeUser, opt_origin);
601           cr.dispatchSimpleEvent(
602               this, DestinationStore.EventType.DESTINATION_SEARCH_STARTED);
603         }
604       }
605     },
607     /** Requests load of COOKIE based cloud destinations. */
608     reloadUserCookieBasedDestinations: function() {
609       var origins = this.loadedCloudOrigins_[this.userInfo_.activeUser] || [];
610       if (origins.indexOf(print_preview.Destination.Origin.COOKIES) >= 0) {
611         cr.dispatchSimpleEvent(
612             this, DestinationStore.EventType.DESTINATION_SEARCH_DONE);
613       } else {
614         this.startLoadCloudDestinations(
615             print_preview.Destination.Origin.COOKIES);
616       }
617     },
619     /** Initiates loading of all known destination types. */
620     startLoadAllDestinations: function() {
621       this.startLoadCloudDestinations();
622       this.startLoadLocalDestinations();
623       this.startLoadPrivetDestinations();
624       this.startLoadExtensionDestinations();
625     },
627     /**
628      * Wait for a privet device to be registered.
629      */
630     waitForRegister: function(id) {
631       this.nativeLayer_.startGetPrivetDestinations();
632       this.waitForRegisterDestination_ = id;
633     },
635     /**
636      * Event handler for {@code
637      * print_preview.NativeLayer.EventType.PROVISIONAL_DESTINATION_RESOLVED}.
638      * Currently assumes the provisional destination is an extension
639      * destination.
640      * Called when a provisional destination resolvement attempt finishes.
641      * The provisional destination is removed from the store and replaced with
642      * a destination created from the resolved destination properties, if any
643      * are reported.
644      * Emits {@code DestinationStore.EventType.PROVISIONAL_DESTINATION_RESOLVED}
645      * event.
646      * @param {!Event} The event containing the provisional destination ID and
647      *     resolved destination description. If the destination was not
648      *     successfully resolved, the description will not be set.
649      * @private
650      */
651     handleProvisionalDestinationResolved_: function(evt) {
652       var provisionalDestinationIndex = -1;
653       var provisionalDestination = null;
654       for (var i = 0; i < this.destinations_.length; ++i) {
655         if (evt.provisionalId == this.destinations_[i].id) {
656           provisionalDestinationIndex = i;
657           provisionalDestination = this.destinations_[i];
658           break;
659         }
660       }
662       if (!provisionalDestination)
663         return;
665       this.destinations_.splice(provisionalDestinationIndex, 1);
666       delete this.destinationMap_[this.getKey_(provisionalDestination)];
668       var destination = evt.destination ?
669           print_preview.ExtensionDestinationParser.parse(evt.destination) :
670           null;
672       if (destination)
673         this.insertIntoStore_(destination);
675       var event = new Event(
676           DestinationStore.EventType.PROVISIONAL_DESTINATION_RESOLVED);
677       event.provisionalId = evt.provisionalId;
678       event.destination = destination;
679       this.dispatchEvent(event);
680     },
682     /**
683      * Inserts {@code destination} to the data store and dispatches a
684      * DESTINATIONS_INSERTED event.
685      * @param {!print_preview.Destination} destination Print destination to
686      *     insert.
687      * @private
688      */
689     insertDestination_: function(destination) {
690       if (this.insertIntoStore_(destination)) {
691         this.destinationsInserted_(destination);
692       }
693     },
695     /**
696      * Inserts multiple {@code destinations} to the data store and dispatches
697      * single DESTINATIONS_INSERTED event.
698      * @param {!Array<print_preview.Destination>} destinations Print
699      *     destinations to insert.
700      * @private
701      */
702     insertDestinations_: function(destinations) {
703       var inserted = false;
704       destinations.forEach(function(destination) {
705         inserted = this.insertIntoStore_(destination) || inserted;
706       }, this);
707       if (inserted) {
708         this.destinationsInserted_();
709       }
710     },
712     /**
713      * Dispatches DESTINATIONS_INSERTED event. In auto select mode, tries to
714      * update selected destination to match {@code appState_} settings.
715      * @param {print_preview.Destination=} opt_destination The only destination
716      *     that was changed or skipped if possibly more than one destination was
717      *     changed. Used as a hint to limit destination search scope in
718      *     {@code isInAutoSelectMode_).
719      */
720     destinationsInserted_: function(opt_destination) {
721       cr.dispatchSimpleEvent(
722           this, DestinationStore.EventType.DESTINATIONS_INSERTED);
723       if (this.isInAutoSelectMode_) {
724         var destinationsToSearch =
725             opt_destination && [opt_destination] || this.destinations_;
726         destinationsToSearch.some(function(destination) {
727           if (this.matchPersistedDestination_(destination)) {
728             this.selectDestination(destination);
729             return true;
730           }
731         }, this);
732       }
733     },
735     /**
736      * Updates an existing print destination with capabilities and display name
737      * information. If the destination doesn't already exist, it will be added.
738      * @param {!print_preview.Destination} destination Destination to update.
739      * @return {!print_preview.Destination} The existing destination that was
740      *     updated or {@code null} if it was the new destination.
741      * @private
742      */
743     updateDestination_: function(destination) {
744       assert(destination.constructor !== Array, 'Single printer expected');
745       var existingDestination = this.destinationMap_[this.getKey_(destination)];
746       if (existingDestination != null) {
747         existingDestination.capabilities = destination.capabilities;
748       } else {
749         this.insertDestination_(destination);
750       }
752       if (existingDestination == this.selectedDestination_ ||
753           destination == this.selectedDestination_) {
754         this.appState_.persistSelectedDestination(this.selectedDestination_);
755         cr.dispatchSimpleEvent(
756             this,
757             DestinationStore.EventType.SELECTED_DESTINATION_CAPABILITIES_READY);
758       }
760       return existingDestination;
761     },
763     /**
764      * Called when the search for Privet printers is done.
765      * @private
766      */
767     endPrivetPrinterSearch_: function() {
768       this.nativeLayer_.stopGetPrivetDestinations();
769       this.isPrivetDestinationSearchInProgress_ = false;
770       this.hasLoadedAllPrivetDestinations_ = true;
771       cr.dispatchSimpleEvent(
772           this, DestinationStore.EventType.DESTINATION_SEARCH_DONE);
773     },
775     /**
776      * Called when loading of extension managed printers is done.
777      * @private
778      */
779     endExtensionPrinterSearch_: function() {
780       this.isExtensionDestinationSearchInProgress_ = false;
781       this.hasLoadedAllExtensionDestinations_ = true;
782       cr.dispatchSimpleEvent(
783           this, DestinationStore.EventType.DESTINATION_SEARCH_DONE);
784       // Clear initially selected (cached) extension destination if it hasn't
785       // been found among reported extension destinations.
786       if (this.isInAutoSelectMode_ && this.selectedDestination_.isExtension)
787         this.selectDefaultDestination_();
788     },
790     /**
791      * Inserts a destination into the store without dispatching any events.
792      * @return {boolean} Whether the inserted destination was not already in the
793      *     store.
794      * @private
795      */
796     insertIntoStore_: function(destination) {
797       var key = this.getKey_(destination);
798       var existingDestination = this.destinationMap_[key];
799       if (existingDestination == null) {
800         this.destinations_.push(destination);
801         this.destinationMap_[key] = destination;
802         return true;
803       } else if (existingDestination.connectionStatus ==
804                      print_preview.Destination.ConnectionStatus.UNKNOWN &&
805                  destination.connectionStatus !=
806                      print_preview.Destination.ConnectionStatus.UNKNOWN) {
807         existingDestination.connectionStatus = destination.connectionStatus;
808         return true;
809       } else {
810         return false;
811       }
812     },
814     /**
815      * Binds handlers to events.
816      * @private
817      */
818     addEventListeners_: function() {
819       this.tracker_.add(
820           this.nativeLayer_,
821           print_preview.NativeLayer.EventType.LOCAL_DESTINATIONS_SET,
822           this.onLocalDestinationsSet_.bind(this));
823       this.tracker_.add(
824           this.nativeLayer_,
825           print_preview.NativeLayer.EventType.CAPABILITIES_SET,
826           this.onLocalDestinationCapabilitiesSet_.bind(this));
827       this.tracker_.add(
828           this.nativeLayer_,
829           print_preview.NativeLayer.EventType.GET_CAPABILITIES_FAIL,
830           this.onGetCapabilitiesFail_.bind(this));
831       this.tracker_.add(
832           this.nativeLayer_,
833           print_preview.NativeLayer.EventType.DESTINATIONS_RELOAD,
834           this.onDestinationsReload_.bind(this));
835       this.tracker_.add(
836           this.nativeLayer_,
837           print_preview.NativeLayer.EventType.PRIVET_PRINTER_CHANGED,
838           this.onPrivetPrinterAdded_.bind(this));
839       this.tracker_.add(
840           this.nativeLayer_,
841           print_preview.NativeLayer.EventType.PRIVET_CAPABILITIES_SET,
842           this.onPrivetCapabilitiesSet_.bind(this));
843       this.tracker_.add(
844           this.nativeLayer_,
845           print_preview.NativeLayer.EventType.EXTENSION_PRINTERS_ADDED,
846           this.onExtensionPrintersAdded_.bind(this));
847       this.tracker_.add(
848           this.nativeLayer_,
849           print_preview.NativeLayer.EventType.EXTENSION_CAPABILITIES_SET,
850           this.onExtensionCapabilitiesSet_.bind(this));
851       this.tracker_.add(
852           this.nativeLayer_,
853           print_preview.NativeLayer.EventType.PROVISIONAL_DESTINATION_RESOLVED,
854           this.handleProvisionalDestinationResolved_.bind(this));
855     },
857     /**
858      * Creates a local PDF print destination.
859      * @return {!print_preview.Destination} Created print destination.
860      * @private
861      */
862     createLocalPdfPrintDestination_: function() {
863       // TODO(alekseys): Create PDF printer in the native code and send its
864       // capabilities back with other local printers.
865       if (this.pdfPrinterEnabled_) {
866         this.insertDestination_(new print_preview.Destination(
867             print_preview.Destination.GooglePromotedId.SAVE_AS_PDF,
868             print_preview.Destination.Type.LOCAL,
869             print_preview.Destination.Origin.LOCAL,
870             loadTimeData.getString('printToPDF'),
871             false /*isRecent*/,
872             print_preview.Destination.ConnectionStatus.ONLINE));
873       }
874     },
876     /**
877      * Resets the state of the destination store to its initial state.
878      * @private
879      */
880     reset_: function() {
881       this.destinations_ = [];
882       this.destinationMap_ = {};
883       this.selectDestination(null);
884       this.loadedCloudOrigins_ = {};
885       this.hasLoadedAllLocalDestinations_ = false;
886       this.hasLoadedAllPrivetDestinations_ = false;
887       this.hasLoadedAllExtensionDestinations_ = false;
889       clearTimeout(this.autoSelectTimeout_);
890       this.autoSelectTimeout_ = setTimeout(
891           this.selectDefaultDestination_.bind(this),
892           DestinationStore.AUTO_SELECT_TIMEOUT_);
893     },
895     /**
896      * Called when the local destinations have been got from the native layer.
897      * @param {Event} event Contains the local destinations.
898      * @private
899      */
900     onLocalDestinationsSet_: function(event) {
901       var localDestinations = event.destinationInfos.map(function(destInfo) {
902         return print_preview.LocalDestinationParser.parse(destInfo);
903       });
904       this.insertDestinations_(localDestinations);
905       this.isLocalDestinationSearchInProgress_ = false;
906       cr.dispatchSimpleEvent(
907           this, DestinationStore.EventType.DESTINATION_SEARCH_DONE);
908     },
910     /**
911      * Called when the native layer retrieves the capabilities for the selected
912      * local destination. Updates the destination with new capabilities if the
913      * destination already exists, otherwise it creates a new destination and
914      * then updates its capabilities.
915      * @param {Event} event Contains the capabilities of the local print
916      *     destination.
917      * @private
918      */
919     onLocalDestinationCapabilitiesSet_: function(event) {
920       var destinationId = event.settingsInfo['printerId'];
921       var key = this.getDestinationKey_(
922           print_preview.Destination.Origin.LOCAL,
923           destinationId,
924           '');
925       var destination = this.destinationMap_[key];
926       var capabilities = DestinationStore.localizeCapabilities_(
927           event.settingsInfo.capabilities);
928       // Special case for PDF printer (until local printers capabilities are
929       // reported in CDD format too).
930       if (destinationId ==
931           print_preview.Destination.GooglePromotedId.SAVE_AS_PDF) {
932         if (destination) {
933           destination.capabilities = capabilities;
934         }
935       } else {
936         if (destination) {
937           // In case there were multiple capabilities request for this local
938           // destination, just ignore the later ones.
939           if (destination.capabilities != null) {
940             return;
941           }
942           destination.capabilities = capabilities;
943         } else {
944           // TODO(rltoscano): This makes the assumption that the "deviceName" is
945           // the same as "printerName". We should include the "printerName" in
946           // the response. See http://crbug.com/132831.
947           destination = print_preview.LocalDestinationParser.parse(
948               {deviceName: destinationId, printerName: destinationId});
949           destination.capabilities = capabilities;
950           this.insertDestination_(destination);
951         }
952       }
953       if (this.selectedDestination_ &&
954           this.selectedDestination_.id == destinationId) {
955         cr.dispatchSimpleEvent(this,
956                                DestinationStore.EventType.
957                                    SELECTED_DESTINATION_CAPABILITIES_READY);
958       }
959     },
961     /**
962      * Called when a request to get a local destination's print capabilities
963      * fails. If the destination is the initial destination, auto-select another
964      * destination instead.
965      * @param {Event} event Contains the destination ID that failed.
966      * @private
967      */
968     onGetCapabilitiesFail_: function(event) {
969       console.error('Failed to get print capabilities for printer ' +
970                     event.destinationId);
971       if (this.isInAutoSelectMode_ &&
972           this.sameAsPersistedDestination_(event.destinationId,
973                                            event.destinationOrigin)) {
974         this.selectDefaultDestination_();
975       }
976     },
978     /**
979      * Called when the /search call completes, either successfully or not.
980      * In case of success, stores fetched destinations.
981      * @param {Event} event Contains the request result.
982      * @private
983      */
984     onCloudPrintSearchDone_: function(event) {
985       if (event.printers) {
986         this.insertDestinations_(event.printers);
987       }
988       if (event.searchDone) {
989         var origins = this.loadedCloudOrigins_[event.user] || [];
990         if (origins.indexOf(event.origin) < 0) {
991           this.loadedCloudOrigins_[event.user] = origins.concat([event.origin]);
992         }
993       }
994       cr.dispatchSimpleEvent(
995           this, DestinationStore.EventType.DESTINATION_SEARCH_DONE);
996     },
998     /**
999      * Called when /printer call completes. Updates the specified destination's
1000      * print capabilities.
1001      * @param {Event} event Contains detailed information about the
1002      *     destination.
1003      * @private
1004      */
1005     onCloudPrintPrinterDone_: function(event) {
1006       this.updateDestination_(event.printer);
1007     },
1009     /**
1010      * Called when the Google Cloud Print interface fails to lookup a
1011      * destination. Selects another destination if the failed destination was
1012      * the initial destination.
1013      * @param {Object} event Contains the ID of the destination that was failed
1014      *     to be looked up.
1015      * @private
1016      */
1017     onCloudPrintPrinterFailed_: function(event) {
1018       if (this.isInAutoSelectMode_ &&
1019           this.sameAsPersistedDestination_(event.destinationId,
1020                                            event.destinationOrigin)) {
1021         console.error(
1022             'Failed to fetch last used printer caps: ' + event.destinationId);
1023         this.selectDefaultDestination_();
1024       }
1025     },
1027     /**
1028      * Called when printer sharing invitation was processed successfully.
1029      * @param {Event} event Contains detailed information about the invite and
1030      *     newly accepted destination (if known).
1031      * @private
1032      */
1033     onCloudPrintProcessInviteDone_: function(event) {
1034       if (event.accept && event.printer) {
1035         // Hint the destination list to promote this new destination.
1036         event.printer.isRecent = true;
1037         this.insertDestination_(event.printer);
1038       }
1039     },
1041     /**
1042      * Called when a Privet printer is added to the local network.
1043      * @param {Object} event Contains information about the added printer.
1044      * @private
1045      */
1046     onPrivetPrinterAdded_: function(event) {
1047       if (event.printer.serviceName == this.waitForRegisterDestination_ &&
1048           !event.printer.isUnregistered) {
1049         this.waitForRegisterDestination_ = null;
1050         this.onDestinationsReload_();
1051       } else {
1052         this.insertDestinations_(
1053             print_preview.PrivetDestinationParser.parse(event.printer));
1054       }
1055     },
1057     /**
1058      * Called when capabilities for a privet printer are set.
1059      * @param {Object} event Contains the capabilities and printer ID.
1060      * @private
1061      */
1062     onPrivetCapabilitiesSet_: function(event) {
1063       var destinations =
1064           print_preview.PrivetDestinationParser.parse(event.printer);
1065       destinations.forEach(function(dest) {
1066         dest.capabilities = event.capabilities;
1067         this.updateDestination_(dest);
1068       }, this);
1069     },
1071     /**
1072      * Called when an extension responds to a getExtensionDestinations
1073      * request.
1074      * @param {Object} event Contains information about list of printers
1075      *     reported by the extension.
1076      *     {@code done} parameter is set iff this is the final list of printers
1077      *     returned as part of getExtensionDestinations request.
1078      * @private
1079      */
1080     onExtensionPrintersAdded_: function(event) {
1081       this.insertDestinations_(event.printers.map(function(printer) {
1082         return print_preview.ExtensionDestinationParser.parse(printer);
1083       }));
1085       if (event.done && this.isExtensionDestinationSearchInProgress_) {
1086         clearTimeout(this.extensionSearchTimeout_);
1087         this.endExtensionPrinterSearch_();
1088       }
1089     },
1091     /**
1092      * Called when capabilities for an extension managed printer are set.
1093      * @param {Object} event Contains the printer's capabilities and ID.
1094      * @private
1095      */
1096     onExtensionCapabilitiesSet_: function(event) {
1097       var destinationKey = this.getDestinationKey_(
1098           print_preview.Destination.Origin.EXTENSION,
1099           event.printerId,
1100           '' /* account */);
1101       var destination = this.destinationMap_[destinationKey];
1102       if (!destination)
1103         return;
1104       destination.capabilities = event.capabilities;
1105       this.updateDestination_(destination);
1106     },
1108     /**
1109      * Called from native layer after the user was requested to sign in, and did
1110      * so successfully.
1111      * @private
1112      */
1113     onDestinationsReload_: function() {
1114       this.reset_();
1115       this.isInAutoSelectMode_ = true;
1116       this.createLocalPdfPrintDestination_();
1117       this.startLoadAllDestinations();
1118     },
1120     // TODO(vitalybuka): Remove three next functions replacing Destination.id
1121     //    and Destination.origin by complex ID.
1122     /**
1123      * Returns key to be used with {@code destinationMap_}.
1124      * @param {!print_preview.Destination.Origin} origin Destination origin.
1125      * @param {string} id Destination id.
1126      * @param {string} account User account destination is registered for.
1127      * @private
1128      */
1129     getDestinationKey_: function(origin, id, account) {
1130       return origin + '/' + id + '/' + account;
1131     },
1133     /**
1134      * Returns key to be used with {@code destinationMap_}.
1135      * @param {!print_preview.Destination} destination Destination.
1136      * @private
1137      */
1138     getKey_: function(destination) {
1139       return this.getDestinationKey_(
1140           destination.origin, destination.id, destination.account);
1141     },
1143     /**
1144      * @param {!print_preview.Destination} destination Destination to match.
1145      * @return {boolean} Whether {@code destination} matches the last user
1146      *     selected one.
1147      * @private
1148      */
1149     matchPersistedDestination_: function(destination) {
1150       return !this.appState_.selectedDestinationId ||
1151              !this.appState_.selectedDestinationOrigin ||
1152              this.sameAsPersistedDestination_(
1153                  destination.id, destination.origin);
1154     },
1156     /**
1157      * @param {?string} id Id of the destination.
1158      * @param {?string} origin Oring of the destination.
1159      * @return {boolean} Whether destination is the same as initial.
1160      * @private
1161      */
1162     sameAsPersistedDestination_: function(id, origin) {
1163       return id == this.appState_.selectedDestinationId &&
1164              origin == this.appState_.selectedDestinationOrigin;
1165     }
1166   };
1168   // Export
1169   return {
1170     DestinationStore: DestinationStore
1171   };