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
5cr.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.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);
20
21    /**
22     * Used to fetch local print destinations.
23     * @type {!print_preview.NativeLayer}
24     * @private
25     */
26    this.nativeLayer_ = nativeLayer;
27
28    /**
29     * User information repository.
30     * @type {!print_preview.UserInfo}
31     * @private
32     */
33    this.userInfo_ = userInfo;
34
35    /**
36     * Used to load and persist the selected destination.
37     * @type {!print_preview.AppState}
38     * @private
39     */
40    this.appState_ = appState;
41
42    /**
43     * Used to track metrics.
44     * @type {!print_preview.DestinationSearchMetricsContext}
45     * @private
46     */
47    this.metrics_ = new print_preview.DestinationSearchMetricsContext();
48
49    /**
50     * Internal backing store for the data store.
51     * @type {!Array.<!print_preview.Destination>}
52     * @private
53     */
54    this.destinations_ = [];
55
56    /**
57     * Cache used for constant lookup of destinations by origin and id.
58     * @type {object.<string, !print_preview.Destination>}
59     * @private
60     */
61    this.destinationMap_ = {};
62
63    /**
64     * Currently selected destination.
65     * @type {print_preview.Destination}
66     * @private
67     */
68    this.selectedDestination_ = null;
69
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;
77
78    /**
79     * Event tracker used to track event listeners of the destination store.
80     * @type {!EventTracker}
81     * @private
82     */
83    this.tracker_ = new EventTracker();
84
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;
92
93    /**
94     * Used to fetch cloud-based print destinations.
95     * @type {print_preview.CloudPrintInterface}
96     * @private
97     */
98    this.cloudPrintInterface_ = null;
99
100    /**
101     * Maps user account to the list of origins for which destinations are
102     * already loaded.
103     * @type {!Object.<string, Array.<print_preview.Destination.Origin>>}
104     * @private
105     */
106    this.loadedCloudOrigins_ = {};
107
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;
117
118    /**
119     * Whether a search for local destinations is in progress.
120     * @type {boolean}
121     * @private
122     */
123    this.isLocalDestinationSearchInProgress_ = false;
124
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;
132
133    /**
134     * Whether a search for privet destinations is in progress.
135     * @type {boolean}
136     * @private
137     */
138    this.isPrivetDestinationSearchInProgress_ = false;
139
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;
147
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;
155
156    /**
157     * MDNS service name of destination that we are waiting to register.
158     * @type {?string}
159     * @private
160     */
161    this.waitForRegisterDestination_ = null;
162
163    this.addEventListeners_();
164    this.reset_();
165  };
166
167  /**
168   * Event types dispatched by the data store.
169   * @enum {string}
170   */
171  DestinationStore.EventType = {
172    DESTINATION_SEARCH_DONE:
173        'print_preview.DestinationStore.DESTINATION_SEARCH_DONE',
174    DESTINATION_SEARCH_STARTED:
175        'print_preview.DestinationStore.DESTINATION_SEARCH_STARTED',
176    DESTINATION_SELECT: 'print_preview.DestinationStore.DESTINATION_SELECT',
177    DESTINATIONS_INSERTED:
178        'print_preview.DestinationStore.DESTINATIONS_INSERTED',
179    CACHED_SELECTED_DESTINATION_INFO_READY:
180        'print_preview.DestinationStore.CACHED_SELECTED_DESTINATION_INFO_READY',
181    SELECTED_DESTINATION_CAPABILITIES_READY:
182        'print_preview.DestinationStore.SELECTED_DESTINATION_CAPABILITIES_READY'
183  };
184
185  /**
186   * Delay in milliseconds before the destination store ignores the initial
187   * destination ID and just selects any printer (since the initial destination
188   * was not found).
189   * @type {number}
190   * @const
191   * @private
192   */
193  DestinationStore.AUTO_SELECT_TIMEOUT_ = 15000;
194
195  /**
196   * Amount of time spent searching for privet destination, in milliseconds.
197   * @type {number}
198   * @const
199   * @private
200   */
201  DestinationStore.PRIVET_SEARCH_DURATION_ = 2000;
202
203  /**
204   * Localizes printer capabilities.
205   * @param {!Object} capabilities Printer capabilities to localize.
206   * @return {!Object} Localized capabilities.
207   * @private
208   */
209  DestinationStore.localizeCapabilities_ = function(capabilities) {
210    var mediaSize = capabilities.printer.media_size;
211    if (mediaSize) {
212      var mediaDisplayNames = {
213        'ISO_A4': 'A4',
214        'ISO_A3': 'A3',
215        'NA_LETTER': 'Letter',
216        'NA_LEGAL': 'Legal',
217        'NA_LEDGER': 'Tabloid'
218      };
219      for (var i = 0, media; media = mediaSize.option[i]; i++) {
220        media.custom_display_name =
221            media.custom_display_name ||
222            mediaDisplayNames[media.name] ||
223            media.name;
224      }
225    }
226    return capabilities;
227  };
228
229  DestinationStore.prototype = {
230    __proto__: cr.EventTarget.prototype,
231
232    /**
233     * @param {string=} opt_account Account to filter destinations by. When
234     *     omitted, all destinations are returned.
235     * @return {!Array.<!print_preview.Destination>} List of destinations
236     *     accessible by the {@code account}.
237     */
238    destinations: function(opt_account) {
239      if (opt_account) {
240        return this.destinations_.filter(function(destination) {
241          return !destination.account || destination.account == opt_account;
242        });
243      } else {
244        return this.destinations_.slice(0);
245      }
246    },
247
248    /**
249     * @return {print_preview.Destination} The currently selected destination or
250     *     {@code null} if none is selected.
251     */
252    get selectedDestination() {
253      return this.selectedDestination_;
254    },
255
256    /** @return {boolean} Whether destination selection is pending or not. */
257    get isAutoSelectDestinationInProgress() {
258      return this.selectedDestination_ == null &&
259          this.autoSelectTimeout_ != null;
260    },
261
262    /**
263     * @return {boolean} Whether a search for local destinations is in progress.
264     */
265    get isLocalDestinationSearchInProgress() {
266      return this.isLocalDestinationSearchInProgress_ ||
267        this.isPrivetDestinationSearchInProgress_;
268    },
269
270    /**
271     * @return {boolean} Whether a search for cloud destinations is in progress.
272     */
273    get isCloudDestinationSearchInProgress() {
274      return this.cloudPrintInterface_ &&
275             this.cloudPrintInterface_.isCloudDestinationSearchInProgress;
276    },
277
278    /**
279     * Initializes the destination store. Sets the initially selected
280     * destination. If any inserted destinations match this ID, that destination
281     * will be automatically selected. This method must be called after the
282     * print_preview.AppState has been initialized.
283     * @param {boolean} isInAppKioskMode Whether the print preview is in App
284     *     Kiosk mode.
285     */
286    init: function(isInAppKioskMode) {
287      this.pdfPrinterEnabled_ = !isInAppKioskMode;
288      this.isInAutoSelectMode_ = true;
289      this.createLocalPdfPrintDestination_();
290      if (!this.appState_.selectedDestinationId ||
291          !this.appState_.selectedDestinationOrigin) {
292        this.selectDefaultDestination_();
293      } else {
294        var key = this.getDestinationKey_(
295            this.appState_.selectedDestinationOrigin,
296            this.appState_.selectedDestinationId,
297            this.appState_.selectedDestinationAccount);
298        var candidate = this.destinationMap_[key];
299        if (candidate != null) {
300          this.selectDestination(candidate);
301        } else if (this.appState_.selectedDestinationOrigin ==
302                   print_preview.Destination.Origin.LOCAL) {
303          this.nativeLayer_.startGetLocalDestinationCapabilities(
304              this.appState_.selectedDestinationId);
305        } else if (this.cloudPrintInterface_ &&
306                   (this.appState_.selectedDestinationOrigin ==
307                        print_preview.Destination.Origin.COOKIES ||
308                    this.appState_.selectedDestinationOrigin ==
309                        print_preview.Destination.Origin.DEVICE)) {
310          this.cloudPrintInterface_.printer(
311              this.appState_.selectedDestinationId,
312              this.appState_.selectedDestinationOrigin,
313              this.appState_.selectedDestinationAccount);
314        } else if (this.appState_.selectedDestinationOrigin ==
315                   print_preview.Destination.Origin.PRIVET) {
316          // TODO(noamsml): Resolve a specific printer instead of listing all
317          // privet printers in this case.
318          this.nativeLayer_.startGetPrivetDestinations();
319
320          var destinationName = this.appState_.selectedDestinationName || '';
321
322          // Create a fake selectedDestination_ that is not actually in the
323          // destination store. When the real destination is created, this
324          // destination will be overwritten.
325          this.selectedDestination_ = new print_preview.Destination(
326              this.appState_.selectedDestinationId,
327              print_preview.Destination.Type.LOCAL,
328              print_preview.Destination.Origin.PRIVET,
329              destinationName,
330              false /*isRecent*/,
331              print_preview.Destination.ConnectionStatus.ONLINE);
332          this.selectedDestination_.capabilities =
333              this.appState_.selectedDestinationCapabilities;
334
335          cr.dispatchSimpleEvent(
336            this,
337            DestinationStore.EventType.CACHED_SELECTED_DESTINATION_INFO_READY);
338        } else {
339          this.selectDefaultDestination_();
340        }
341      }
342    },
343
344    /**
345     * Sets the destination store's Google Cloud Print interface.
346     * @param {!print_preview.CloudPrintInterface} cloudPrintInterface Interface
347     *     to set.
348     */
349    setCloudPrintInterface: function(cloudPrintInterface) {
350      this.cloudPrintInterface_ = cloudPrintInterface;
351      this.tracker_.add(
352          this.cloudPrintInterface_,
353          cloudprint.CloudPrintInterface.EventType.SEARCH_DONE,
354          this.onCloudPrintSearchDone_.bind(this));
355      this.tracker_.add(
356          this.cloudPrintInterface_,
357          cloudprint.CloudPrintInterface.EventType.SEARCH_FAILED,
358          this.onCloudPrintSearchDone_.bind(this));
359      this.tracker_.add(
360          this.cloudPrintInterface_,
361          cloudprint.CloudPrintInterface.EventType.PRINTER_DONE,
362          this.onCloudPrintPrinterDone_.bind(this));
363      this.tracker_.add(
364          this.cloudPrintInterface_,
365          cloudprint.CloudPrintInterface.EventType.PRINTER_FAILED,
366          this.onCloudPrintPrinterFailed_.bind(this));
367      this.tracker_.add(
368          this.cloudPrintInterface_,
369          cloudprint.CloudPrintInterface.EventType.PROCESS_INVITE_DONE,
370          this.onCloudPrintProcessInviteDone_.bind(this));
371    },
372
373    /**
374     * @return {boolean} Whether only default cloud destinations have been
375     *     loaded.
376     */
377    hasOnlyDefaultCloudDestinations: function() {
378      // TODO: Move the logic to print_preview.
379      return this.destinations_.every(function(dest) {
380        return dest.isLocal ||
381            dest.id == print_preview.Destination.GooglePromotedId.DOCS ||
382            dest.id == print_preview.Destination.GooglePromotedId.FEDEX;
383      });
384    },
385
386    /**
387     * @param {!print_preview.Destination} destination Destination to select.
388     */
389    selectDestination: function(destination) {
390      this.isInAutoSelectMode_ = false;
391      // When auto select expires, DESTINATION_SELECT event has to be dispatched
392      // anyway (see isAutoSelectDestinationInProgress() logic).
393      if (this.autoSelectTimeout_) {
394        clearTimeout(this.autoSelectTimeout_);
395        this.autoSelectTimeout_ = null;
396      } else if (destination == this.selectedDestination_) {
397        return;
398      }
399      if (destination == null) {
400        this.selectedDestination_ = null;
401        cr.dispatchSimpleEvent(
402            this, DestinationStore.EventType.DESTINATION_SELECT);
403        return;
404      }
405      // Update and persist selected destination.
406      this.selectedDestination_ = destination;
407      this.selectedDestination_.isRecent = true;
408      if (destination.id == print_preview.Destination.GooglePromotedId.FEDEX &&
409          !destination.isTosAccepted) {
410        assert(this.cloudPrintInterface_ != null,
411               'Selected FedEx destination, but GCP API is not available');
412        destination.isTosAccepted = true;
413        this.cloudPrintInterface_.updatePrinterTosAcceptance(destination, true);
414      }
415      this.appState_.persistSelectedDestination(this.selectedDestination_);
416      // Adjust metrics.
417      if (destination.cloudID &&
418          this.destinations_.some(function(otherDestination) {
419            return otherDestination.cloudID == destination.cloudID &&
420                otherDestination != destination;
421          })) {
422        this.metrics_.record(destination.isPrivet ?
423            print_preview.Metrics.DestinationSearchBucket.
424                PRIVET_DUPLICATE_SELECTED :
425            print_preview.Metrics.DestinationSearchBucket.
426                CLOUD_DUPLICATE_SELECTED);
427      }
428      // Notify about selected destination change.
429      cr.dispatchSimpleEvent(
430          this, DestinationStore.EventType.DESTINATION_SELECT);
431      // Request destination capabilities, of not known yet.
432      if (destination.capabilities == null) {
433        if (destination.isPrivet) {
434          this.nativeLayer_.startGetPrivetDestinationCapabilities(
435              destination.id);
436        }
437        else if (destination.isLocal) {
438          this.nativeLayer_.startGetLocalDestinationCapabilities(
439              destination.id);
440        } else {
441          assert(this.cloudPrintInterface_ != null,
442                 'Cloud destination selected, but GCP is not enabled');
443          this.cloudPrintInterface_.printer(
444              destination.id, destination.origin, destination.account);
445        }
446      } else {
447        cr.dispatchSimpleEvent(
448            this,
449            DestinationStore.EventType.SELECTED_DESTINATION_CAPABILITIES_READY);
450      }
451    },
452
453    /**
454     * Selects 'Save to PDF' destination (since it always exists).
455     * @private
456     */
457    selectDefaultDestination_: function() {
458      var saveToPdfKey = this.getDestinationKey_(
459          print_preview.Destination.Origin.LOCAL,
460          print_preview.Destination.GooglePromotedId.SAVE_AS_PDF,
461          '');
462      this.selectDestination(
463          this.destinationMap_[saveToPdfKey] || this.destinations_[0] || null);
464    },
465
466    /** Initiates loading of local print destinations. */
467    startLoadLocalDestinations: function() {
468      if (!this.hasLoadedAllLocalDestinations_) {
469        this.hasLoadedAllLocalDestinations_ = true;
470        this.nativeLayer_.startGetLocalDestinations();
471        this.isLocalDestinationSearchInProgress_ = true;
472        cr.dispatchSimpleEvent(
473            this, DestinationStore.EventType.DESTINATION_SEARCH_STARTED);
474      }
475    },
476
477    /** Initiates loading of privet print destinations. */
478    startLoadPrivetDestinations: function() {
479      if (!this.hasLoadedAllPrivetDestinations_) {
480        this.isPrivetDestinationSearchInProgress_ = true;
481        this.nativeLayer_.startGetPrivetDestinations();
482        cr.dispatchSimpleEvent(
483            this, DestinationStore.EventType.DESTINATION_SEARCH_STARTED);
484        this.privetSearchTimeout_ = setTimeout(
485            this.endPrivetPrinterSearch_.bind(this),
486            DestinationStore.PRIVET_SEARCH_DURATION_);
487      }
488    },
489
490    /**
491     * Initiates loading of cloud destinations.
492     * @param {print_preview.Destination.Origin=} opt_origin Search destinations
493     *     for the specified origin only.
494     */
495    startLoadCloudDestinations: function(opt_origin) {
496      if (this.cloudPrintInterface_ != null) {
497        var origins = this.loadedCloudOrigins_[this.userInfo_.activeUser] || [];
498        if (origins.length == 0 ||
499            (opt_origin && origins.indexOf(opt_origin) < 0)) {
500          this.cloudPrintInterface_.search(
501              this.userInfo_.activeUser, opt_origin);
502          cr.dispatchSimpleEvent(
503              this, DestinationStore.EventType.DESTINATION_SEARCH_STARTED);
504        }
505      }
506    },
507
508    /** Requests load of COOKIE based cloud destinations. */
509    reloadUserCookieBasedDestinations: function() {
510      var origins = this.loadedCloudOrigins_[this.userInfo_.activeUser] || [];
511      if (origins.indexOf(print_preview.Destination.Origin.COOKIES) >= 0) {
512        cr.dispatchSimpleEvent(
513            this, DestinationStore.EventType.DESTINATION_SEARCH_DONE);
514      } else {
515        this.startLoadCloudDestinations(
516            print_preview.Destination.Origin.COOKIES);
517      }
518    },
519
520    /** Initiates loading of all known destination types. */
521    startLoadAllDestinations: function() {
522      this.startLoadCloudDestinations();
523      this.startLoadLocalDestinations();
524      this.startLoadPrivetDestinations();
525    },
526
527    /**
528     * Wait for a privet device to be registered.
529     */
530    waitForRegister: function(id) {
531      this.nativeLayer_.startGetPrivetDestinations();
532      this.waitForRegisterDestination_ = id;
533    },
534
535    /**
536     * Inserts {@code destination} to the data store and dispatches a
537     * DESTINATIONS_INSERTED event.
538     * @param {!print_preview.Destination} destination Print destination to
539     *     insert.
540     * @private
541     */
542    insertDestination_: function(destination) {
543      if (this.insertIntoStore_(destination)) {
544        this.destinationsInserted_(destination);
545      }
546    },
547
548    /**
549     * Inserts multiple {@code destinations} to the data store and dispatches
550     * single DESTINATIONS_INSERTED event.
551     * @param {!Array.<print_preview.Destination>} destinations Print
552     *     destinations to insert.
553     * @private
554     */
555    insertDestinations_: function(destinations) {
556      var inserted = false;
557      destinations.forEach(function(destination) {
558        inserted = this.insertIntoStore_(destination) || inserted;
559      }, this);
560      if (inserted) {
561        this.destinationsInserted_();
562      }
563    },
564
565    /**
566     * Dispatches DESTINATIONS_INSERTED event. In auto select mode, tries to
567     * update selected destination to match {@code appState_} settings.
568     * @param {print_preview.Destination=} opt_destination The only destination
569     *     that was changed or skipped if possibly more than one destination was
570     *     changed. Used as a hint to limit destination search scope in
571     *     {@code isInAutoSelectMode_).
572     */
573    destinationsInserted_: function(opt_destination) {
574      cr.dispatchSimpleEvent(
575          this, DestinationStore.EventType.DESTINATIONS_INSERTED);
576      if (this.isInAutoSelectMode_) {
577        var destinationsToSearch =
578            opt_destination && [opt_destination] || this.destinations_;
579        destinationsToSearch.some(function(destination) {
580          if (this.matchPersistedDestination_(destination)) {
581            this.selectDestination(destination);
582            return true;
583          }
584        }, this);
585      }
586    },
587
588    /**
589     * Updates an existing print destination with capabilities and display name
590     * information. If the destination doesn't already exist, it will be added.
591     * @param {!print_preview.Destination} destination Destination to update.
592     * @return {!print_preview.Destination} The existing destination that was
593     *     updated or {@code null} if it was the new destination.
594     * @private
595     */
596    updateDestination_: function(destination) {
597      assert(destination.constructor !== Array, 'Single printer expected');
598      var existingDestination = this.destinationMap_[this.getKey_(destination)];
599      if (existingDestination != null) {
600        existingDestination.capabilities = destination.capabilities;
601      } else {
602        this.insertDestination_(destination);
603      }
604
605      if (existingDestination == this.selectedDestination_ ||
606          destination == this.selectedDestination_) {
607        this.appState_.persistSelectedDestination(this.selectedDestination_);
608        cr.dispatchSimpleEvent(
609            this,
610            DestinationStore.EventType.SELECTED_DESTINATION_CAPABILITIES_READY);
611      }
612
613      return existingDestination;
614    },
615
616    /**
617     * Called when the search for Privet printers is done.
618     * @private
619     */
620    endPrivetPrinterSearch_: function() {
621      this.nativeLayer_.stopGetPrivetDestinations();
622      this.isPrivetDestinationSearchInProgress_ = false;
623      this.hasLoadedAllPrivetDestinations_ = true;
624      cr.dispatchSimpleEvent(
625          this, DestinationStore.EventType.DESTINATION_SEARCH_DONE);
626    },
627
628    /**
629     * Inserts a destination into the store without dispatching any events.
630     * @return {boolean} Whether the inserted destination was not already in the
631     *     store.
632     * @private
633     */
634    insertIntoStore_: function(destination) {
635      var key = this.getKey_(destination);
636      var existingDestination = this.destinationMap_[key];
637      if (existingDestination == null) {
638        this.destinations_.push(destination);
639        this.destinationMap_[key] = destination;
640        return true;
641      } else if (existingDestination.connectionStatus ==
642                     print_preview.Destination.ConnectionStatus.UNKNOWN &&
643                 destination.connectionStatus !=
644                     print_preview.Destination.ConnectionStatus.UNKNOWN) {
645        existingDestination.connectionStatus = destination.connectionStatus;
646        return true;
647      } else {
648        return false;
649      }
650    },
651
652    /**
653     * Binds handlers to events.
654     * @private
655     */
656    addEventListeners_: function() {
657      this.tracker_.add(
658          this.nativeLayer_,
659          print_preview.NativeLayer.EventType.LOCAL_DESTINATIONS_SET,
660          this.onLocalDestinationsSet_.bind(this));
661      this.tracker_.add(
662          this.nativeLayer_,
663          print_preview.NativeLayer.EventType.CAPABILITIES_SET,
664          this.onLocalDestinationCapabilitiesSet_.bind(this));
665      this.tracker_.add(
666          this.nativeLayer_,
667          print_preview.NativeLayer.EventType.GET_CAPABILITIES_FAIL,
668          this.onGetCapabilitiesFail_.bind(this));
669      this.tracker_.add(
670          this.nativeLayer_,
671          print_preview.NativeLayer.EventType.DESTINATIONS_RELOAD,
672          this.onDestinationsReload_.bind(this));
673      this.tracker_.add(
674          this.nativeLayer_,
675          print_preview.NativeLayer.EventType.PRIVET_PRINTER_CHANGED,
676          this.onPrivetPrinterAdded_.bind(this));
677      this.tracker_.add(
678          this.nativeLayer_,
679          print_preview.NativeLayer.EventType.PRIVET_CAPABILITIES_SET,
680          this.onPrivetCapabilitiesSet_.bind(this));
681    },
682
683    /**
684     * Creates a local PDF print destination.
685     * @return {!print_preview.Destination} Created print destination.
686     * @private
687     */
688    createLocalPdfPrintDestination_: function() {
689      // TODO(alekseys): Create PDF printer in the native code and send its
690      // capabilities back with other local printers.
691      if (this.pdfPrinterEnabled_) {
692        this.insertDestination_(new print_preview.Destination(
693            print_preview.Destination.GooglePromotedId.SAVE_AS_PDF,
694            print_preview.Destination.Type.LOCAL,
695            print_preview.Destination.Origin.LOCAL,
696            loadTimeData.getString('printToPDF'),
697            false /*isRecent*/,
698            print_preview.Destination.ConnectionStatus.ONLINE));
699      }
700    },
701
702    /**
703     * Resets the state of the destination store to its initial state.
704     * @private
705     */
706    reset_: function() {
707      this.destinations_ = [];
708      this.destinationMap_ = {};
709      this.selectDestination(null);
710      this.loadedCloudOrigins_ = {};
711      this.hasLoadedAllLocalDestinations_ = false;
712
713      clearTimeout(this.autoSelectTimeout_);
714      this.autoSelectTimeout_ = setTimeout(
715          this.selectDefaultDestination_.bind(this),
716          DestinationStore.AUTO_SELECT_TIMEOUT_);
717    },
718
719    /**
720     * Called when the local destinations have been got from the native layer.
721     * @param {Event} event Contains the local destinations.
722     * @private
723     */
724    onLocalDestinationsSet_: function(event) {
725      var localDestinations = event.destinationInfos.map(function(destInfo) {
726        return print_preview.LocalDestinationParser.parse(destInfo);
727      });
728      this.insertDestinations_(localDestinations);
729      this.isLocalDestinationSearchInProgress_ = false;
730      cr.dispatchSimpleEvent(
731          this, DestinationStore.EventType.DESTINATION_SEARCH_DONE);
732    },
733
734    /**
735     * Called when the native layer retrieves the capabilities for the selected
736     * local destination. Updates the destination with new capabilities if the
737     * destination already exists, otherwise it creates a new destination and
738     * then updates its capabilities.
739     * @param {Event} event Contains the capabilities of the local print
740     *     destination.
741     * @private
742     */
743    onLocalDestinationCapabilitiesSet_: function(event) {
744      var destinationId = event.settingsInfo['printerId'];
745      var key = this.getDestinationKey_(
746          print_preview.Destination.Origin.LOCAL,
747          destinationId,
748          '');
749      var destination = this.destinationMap_[key];
750      var capabilities = DestinationStore.localizeCapabilities_(
751          event.settingsInfo.capabilities);
752      // Special case for PDF printer (until local printers capabilities are
753      // reported in CDD format too).
754      if (destinationId ==
755          print_preview.Destination.GooglePromotedId.SAVE_AS_PDF) {
756        if (destination) {
757          destination.capabilities = capabilities;
758        }
759      } else {
760        if (destination) {
761          // In case there were multiple capabilities request for this local
762          // destination, just ignore the later ones.
763          if (destination.capabilities != null) {
764            return;
765          }
766          destination.capabilities = capabilities;
767        } else {
768          // TODO(rltoscano): This makes the assumption that the "deviceName" is
769          // the same as "printerName". We should include the "printerName" in
770          // the response. See http://crbug.com/132831.
771          destination = print_preview.LocalDestinationParser.parse(
772              {deviceName: destinationId, printerName: destinationId});
773          destination.capabilities = capabilities;
774          this.insertDestination_(destination);
775        }
776      }
777      if (this.selectedDestination_ &&
778          this.selectedDestination_.id == destinationId) {
779        cr.dispatchSimpleEvent(this,
780                               DestinationStore.EventType.
781                                   SELECTED_DESTINATION_CAPABILITIES_READY);
782      }
783    },
784
785    /**
786     * Called when a request to get a local destination's print capabilities
787     * fails. If the destination is the initial destination, auto-select another
788     * destination instead.
789     * @param {Event} event Contains the destination ID that failed.
790     * @private
791     */
792    onGetCapabilitiesFail_: function(event) {
793      console.error('Failed to get print capabilities for printer ' +
794                    event.destinationId);
795      if (this.isInAutoSelectMode_ &&
796          this.sameAsPersistedDestination_(event.destinationId,
797                                           event.destinationOrigin)) {
798        this.selectDefaultDestination_();
799      }
800    },
801
802    /**
803     * Called when the /search call completes, either successfully or not.
804     * In case of success, stores fetched destinations.
805     * @param {Event} event Contains the request result.
806     * @private
807     */
808    onCloudPrintSearchDone_: function(event) {
809      if (event.printers) {
810        this.insertDestinations_(event.printers);
811      }
812      if (event.searchDone) {
813        var origins = this.loadedCloudOrigins_[event.user] || [];
814        if (origins.indexOf(event.origin) < 0) {
815          this.loadedCloudOrigins_[event.user] = origins.concat([event.origin]);
816        }
817      }
818      cr.dispatchSimpleEvent(
819          this, DestinationStore.EventType.DESTINATION_SEARCH_DONE);
820    },
821
822    /**
823     * Called when /printer call completes. Updates the specified destination's
824     * print capabilities.
825     * @param {Event} event Contains detailed information about the
826     *     destination.
827     * @private
828     */
829    onCloudPrintPrinterDone_: function(event) {
830      this.updateDestination_(event.printer);
831    },
832
833    /**
834     * Called when the Google Cloud Print interface fails to lookup a
835     * destination. Selects another destination if the failed destination was
836     * the initial destination.
837     * @param {object} event Contains the ID of the destination that was failed
838     *     to be looked up.
839     * @private
840     */
841    onCloudPrintPrinterFailed_: function(event) {
842      if (this.isInAutoSelectMode_ &&
843          this.sameAsPersistedDestination_(event.destinationId,
844                                           event.destinationOrigin)) {
845        console.error(
846            'Failed to fetch last used printer caps: ' + event.destinationId);
847        this.selectDefaultDestination_();
848      }
849    },
850
851    /**
852     * Called when printer sharing invitation was processed successfully.
853     * @param {Event} event Contains detailed information about the invite and
854     *     newly accepted destination (if known).
855     * @private
856     */
857    onCloudPrintProcessInviteDone_: function(event) {
858      if (event.accept && event.printer) {
859        // Hint the destination list to promote this new destination.
860        event.printer.isRecent = true;
861        this.insertDestination_(event.printer);
862      }
863    },
864
865    /**
866     * Called when a Privet printer is added to the local network.
867     * @param {object} event Contains information about the added printer.
868     * @private
869     */
870    onPrivetPrinterAdded_: function(event) {
871      if (event.printer.serviceName == this.waitForRegisterDestination_ &&
872          !event.printer.isUnregistered) {
873        this.waitForRegisterDestination_ = null;
874        this.onDestinationsReload_();
875      } else {
876        this.insertDestinations_(
877            print_preview.PrivetDestinationParser.parse(event.printer));
878      }
879    },
880
881    /**
882     * Called when capabilities for a privet printer are set.
883     * @param {object} event Contains the capabilities and printer ID.
884     * @private
885     */
886    onPrivetCapabilitiesSet_: function(event) {
887      var destinationId = event.printerId;
888      var destinations =
889          print_preview.PrivetDestinationParser.parse(event.printer);
890      destinations.forEach(function(dest) {
891        dest.capabilities = event.capabilities;
892        this.updateDestination_(dest);
893      }, this);
894    },
895
896    /**
897     * Called from native layer after the user was requested to sign in, and did
898     * so successfully.
899     * @private
900     */
901    onDestinationsReload_: function() {
902      this.reset_();
903      this.isInAutoSelectMode_ = true;
904      this.createLocalPdfPrintDestination_();
905      this.startLoadAllDestinations();
906    },
907
908    // TODO(vitalybuka): Remove three next functions replacing Destination.id
909    //    and Destination.origin by complex ID.
910    /**
911     * Returns key to be used with {@code destinationMap_}.
912     * @param {!print_preview.Destination.Origin} origin Destination origin.
913     * @return {string} id Destination id.
914     * @return {string} account User account destination is registered for.
915     * @private
916     */
917    getDestinationKey_: function(origin, id, account) {
918      return origin + '/' + id + '/' + account;
919    },
920
921    /**
922     * Returns key to be used with {@code destinationMap_}.
923     * @param {!print_preview.Destination} destination Destination.
924     * @private
925     */
926    getKey_: function(destination) {
927      return this.getDestinationKey_(
928          destination.origin, destination.id, destination.account);
929    },
930
931    /**
932     * @param {!print_preview.Destination} destination Destination to match.
933     * @return {boolean} Whether {@code destination} matches the last user
934     *     selected one.
935     * @private
936     */
937    matchPersistedDestination_: function(destination) {
938      return !this.appState_.selectedDestinationId ||
939             !this.appState_.selectedDestinationOrigin ||
940             this.sameAsPersistedDestination_(
941                 destination.id, destination.origin);
942    },
943
944    /**
945     * @param {?string} id Id of the destination.
946     * @param {?string} origin Oring of the destination.
947     * @return {boolean} Whether destination is the same as initial.
948     * @private
949     */
950    sameAsPersistedDestination_: function(id, origin) {
951      return id == this.appState_.selectedDestinationId &&
952             origin == this.appState_.selectedDestinationOrigin;
953    }
954  };
955
956  // Export
957  return {
958    DestinationStore: DestinationStore
959  };
960});
961