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() {
9 * A data store that stores destinations and dispatches events when the data
11 * @param {!print_preview.NativeLayer} nativeLayer Used to fetch local print
13 * @param {!print_preview.AppState} appState Application state.
15 * @extends {cr.EventTarget}
17 function DestinationStore(nativeLayer, appState) {
18 cr.EventTarget.call(this);
21 * Used to fetch local print destinations.
22 * @type {!print_preview.NativeLayer}
25 this.nativeLayer_ = nativeLayer;
28 * Used to load and persist the selected destination.
29 * @type {!print_preview.AppState}
32 this.appState_ = appState;
35 * Internal backing store for the data store.
36 * @type {!Array.<!print_preview.Destination>}
39 this.destinations_ = [];
42 * Cache used for constant lookup of destinations by origin and id.
43 * @type {object.<string, !print_preview.Destination>}
46 this.destinationMap_ = {};
49 * Currently selected destination.
50 * @type {print_preview.Destination}
53 this.selectedDestination_ = null;
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.
62 this.initialDestinationId_ = null;
65 * Initial origin used to auto-select destination.
66 * @type {print_preview.Destination.Origin}
69 this.initialDestinationOrigin_ = print_preview.Destination.Origin.LOCAL;
72 * Whether the destination store will auto select the destination that
73 * matches the initial destination.
77 this.isInAutoSelectMode_ = false;
80 * Event tracker used to track event listeners of the destination store.
81 * @type {!EventTracker}
84 this.tracker_ = new EventTracker();
87 * Used to fetch cloud-based print destinations.
88 * @type {print_preview.CloudPrintInterface}
91 this.cloudPrintInterface_ = null;
94 * Whether the destination store has already loaded or is loading all cloud
99 this.hasLoadedAllCloudDestinations_ = false;
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
109 this.autoSelectTimeout_ = null;
112 * Whether a search for local destinations is in progress.
116 this.isLocalDestinationSearchInProgress_ = false;
118 this.addEventListeners_();
123 * Event types dispatched by the data store.
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'
139 * Delay in milliseconds before the destination store ignores the initial
140 * destination ID and just selects any printer (since the initial destination
146 DestinationStore.AUTO_SELECT_TIMEOUT_ = 15000;
149 * Creates a local PDF print destination.
150 * @return {!print_preview.Destination} Created print destination.
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'),
160 print_preview.Destination.ConnectionStatus.ONLINE);
161 dest.capabilities = {
166 {type: 'AUTO', is_default: true},
171 color: { option: [{type: 'STANDARD_COLOR', is_default: true}] }
177 DestinationStore.prototype = {
178 __proto__: cr.EventTarget.prototype,
181 * @return {!Array.<!print_preview.Destination>} List of destinations in
185 return this.destinations_.slice(0);
189 * @return {print_preview.Destination} The currently selected destination or
190 * {@code null} if none is selected.
192 get selectedDestination() {
193 return this.selectedDestination_;
197 * @return {boolean} Whether a search for local destinations is in progress.
199 get isLocalDestinationSearchInProgress() {
200 return this.isLocalDestinationSearchInProgress_;
204 * @return {boolean} Whether a search for cloud destinations is in progress.
206 get isCloudDestinationSearchInProgress() {
207 return this.cloudPrintInterface_ &&
208 this.cloudPrintInterface_.isCloudDestinationSearchInProgress;
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
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;
227 this.initialDestinationId_ = systemDefaultDestinationId;
228 this.initialDestinationOrigin_ = print_preview.Destination.Origin.LOCAL;
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]);
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_);
252 * Sets the destination store's Google Cloud Print interface.
253 * @param {!print_preview.CloudPrintInterface} cloudPrintInterface Interface
256 setCloudPrintInterface: function(cloudPrintInterface) {
257 this.cloudPrintInterface_ = cloudPrintInterface;
259 this.cloudPrintInterface_,
260 cloudprint.CloudPrintInterface.EventType.SEARCH_DONE,
261 this.onCloudPrintSearchDone_.bind(this));
263 this.cloudPrintInterface_,
264 cloudprint.CloudPrintInterface.EventType.SEARCH_FAILED,
265 this.onCloudPrintSearchFailed_.bind(this));
267 this.cloudPrintInterface_,
268 cloudprint.CloudPrintInterface.EventType.PRINTER_DONE,
269 this.onCloudPrintPrinterDone_.bind(this));
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);
283 * @return {boolean} Whether only default cloud destinations have been
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;
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;
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 ' +
308 destination.isTosAccepted = true;
309 this.cloudPrintInterface_.updatePrinterTosAcceptance(destination.id,
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(
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,
328 cr.dispatchSimpleEvent(
330 DestinationStore.EventType.SELECTED_DESTINATION_CAPABILITIES_READY);
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
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);
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.
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;
372 if (insertedDestination) {
373 cr.dispatchSimpleEvent(
374 this, DestinationStore.EventType.DESTINATIONS_INSERTED);
376 if (destinationToAutoSelect != null) {
377 this.selectDestination(destinationToAutoSelect);
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.
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;
395 this.insertDestination(destination);
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);
409 * Initiates loading of cloud destinations.
410 * @param {boolean} recentOnly Whether the load recet destinations only.
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);
424 * Inserts a destination into the store without dispatching any events.
425 * @return {boolean} Whether the inserted destination was not already in the
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;
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;
448 * Binds handlers to events.
451 addEventListeners_: function() {
454 print_preview.NativeLayer.EventType.LOCAL_DESTINATIONS_SET,
455 this.onLocalDestinationsSet_.bind(this));
458 print_preview.NativeLayer.EventType.CAPABILITIES_SET,
459 this.onLocalDestinationCapabilitiesSet_.bind(this));
462 print_preview.NativeLayer.EventType.GET_CAPABILITIES_FAIL,
463 this.onGetCapabilitiesFail_.bind(this));
466 print_preview.NativeLayer.EventType.DESTINATIONS_RELOAD,
467 this.onDestinationsReload_.bind(this));
471 * Resets the state of the destination store to its initial state.
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_);
487 * Called when the local destinations have been got from the native layer.
488 * @param {Event} Contains the local destinations.
491 onLocalDestinationsSet_: function(event) {
492 var localDestinations = event.destinationInfos.map(function(destInfo) {
493 return print_preview.LocalDestinationParser.parse(destInfo);
495 this.insertDestinations(localDestinations);
496 this.isLocalDestinationSearchInProgress_ = false;
497 cr.dispatchSimpleEvent(
498 this, DestinationStore.EventType.DESTINATION_SEARCH_DONE);
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
510 onLocalDestinationCapabilitiesSet_: function(event) {
511 var destinationId = event.settingsInfo['printerId'];
513 this.getDestinationKey_(print_preview.Destination.Origin.LOCAL,
515 var destination = this.destinationMap_[key];
516 var capabilities = print_preview.LocalCapabilitiesParser.parse(
519 // In case there were multiple capabilities request for this local
520 // destination, just ignore the later ones.
521 if (destination.capabilities != null) {
524 destination.capabilities = capabilities;
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);
534 if (this.selectedDestination_ &&
535 this.selectedDestination_.id == destinationId) {
536 cr.dispatchSimpleEvent(this,
537 DestinationStore.EventType.
538 SELECTED_DESTINATION_CAPABILITIES_READY);
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.
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 ' +
558 this.selectDestination(this.destinations_[0]);
563 * Called when the /search call completes. Adds the fetched destinations to
564 * the destination store.
565 * @param {Event} event Contains the fetched destinations.
568 onCloudPrintSearchDone_: function(event) {
569 this.insertDestinations(event.printers);
570 cr.dispatchSimpleEvent(
571 this, DestinationStore.EventType.DESTINATION_SEARCH_DONE);
575 * Called when the /search call fails. Updates outstanding request count and
576 * dispatches CLOUD_DESTINATIONS_LOADED event.
579 onCloudPrintSearchFailed_: function() {
580 cr.dispatchSimpleEvent(
581 this, DestinationStore.EventType.DESTINATION_SEARCH_DONE);
585 * Called when /printer call completes. Updates the specified destination's
586 * print capabilities.
587 * @param {Event} event Contains detailed information about the
591 onCloudPrintPrinterDone_: function(event) {
592 var dest = this.updateDestination(event.printer);
593 if (this.selectedDestination_ == dest) {
594 cr.dispatchSimpleEvent(
596 DestinationStore.EventType.SELECTED_DESTINATION_CAPABILITIES_READY);
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
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 ' +
616 this.selectDestination(this.destinations_[0]);
621 * Called from native layer after the user was requested to sign in, and did
625 onDestinationsReload_: function() {
627 this.isInAutoSelectMode_ = true;
628 this.startLoadLocalDestinations();
629 this.startLoadCloudDestinations(true);
630 this.startLoadCloudDestinations(false);
634 * Called when no destination was auto-selected after some timeout. Selects
635 * the first destination in store.
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]);
645 // TODO(vitalybuka): Remove three next functions replacing Destination.id
646 // and Destination.origin by complex ID.
648 * Returns key to be used with {@code destinationMap_}.
649 * @param {!print_preview.Destination.Origin} origin Destination origin.
650 * @return {!string} id Destination id.
653 getDestinationKey_: function(origin, id) {
654 return origin + '/' + id;
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.
663 matchInitialDestination_: function(id, origin) {
664 return this.initialDestinationId_ == null ||
665 this.initialDestinationOrigin_ == null ||
666 this.matchInitialDestinationStrict_(id, origin);
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.
675 matchInitialDestinationStrict_: function(id, origin) {
676 return id == this.initialDestinationId_ &&
677 origin == this.initialDestinationOrigin_;
683 DestinationStore: DestinationStore