// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assert} from 'chrome://resources/js/assert.js';
import {EventTracker} from 'chrome://resources/js/event_tracker.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import type {CapabilitiesResponse, NativeLayer} from '../native_layer.js';
import {NativeLayerImpl} from '../native_layer.js';
// <if expr="is_chromeos">
import type {NativeLayerCros, PrinterSetupResponse} from '../native_layer_cros.js';
import {NativeLayerCrosImpl} from '../native_layer_cros.js';
// </if>
import type {Cdd, MediaSizeOption} from './cdd.js';
import type {RecentDestination} from './destination.js';
import {createDestinationKey, createRecentDestinationKey, Destination, DestinationOrigin, GooglePromotedDestinationId, isPdfPrinter, PDF_DESTINATION_KEY, PrinterType} from './destination.js';
// <if expr="is_chromeos">
import {DestinationProvisionalType} from './destination.js';
// </if>
import {DestinationMatch} from './destination_match.js';
import type {ExtensionDestinationInfo, LocalDestinationInfo} from './local_parsers.js';
import {parseDestination} from './local_parsers.js';
// <if expr="is_chromeos">
import {parseExtensionDestination} from './local_parsers.js';
import {getStatusReasonFromPrinterStatus, PrinterStatusReason} from './printer_status_cros.js';
// </if>
/**
* Printer search statuses used by the destination store.
*/
enum DestinationStorePrinterSearchStatus {
START = 'start',
SEARCHING = 'searching',
DONE = 'done'
}
/**
* Enumeration of possible destination errors.
*/
export enum DestinationErrorType {
INVALID = 0,
NO_DESTINATIONS = 1,
}
/**
* Localizes printer capabilities.
* @param capabilities Printer capabilities to localize.
* @return Localized capabilities.
*/
function localizeCapabilities(capabilities: Cdd): Cdd {
if (!capabilities.printer) {
return capabilities;
}
const mediaSize = capabilities.printer.media_size;
if (!mediaSize) {
return capabilities;
}
for (let i = 0, media; (media = mediaSize.option[i]); i++) {
// No need to patch capabilities with localized names provided.
if (!media.custom_display_name_localized) {
media.custom_display_name = media.custom_display_name ||
MEDIA_DISPLAY_NAMES_[media.name!] || media.name;
}
}
return capabilities;
}
/**
* Compare two media sizes by their names.
* @return 1 if a > b, -1 if a < b, or 0 if a === b.
*/
function compareMediaNames(a: MediaSizeOption, b: MediaSizeOption): number {
const nameA = a.custom_display_name_localized || a.custom_display_name || '';
const nameB = b.custom_display_name_localized || b.custom_display_name || '';
return nameA === nameB ? 0 : (nameA > nameB ? 1 : -1);
}
/**
* Sort printer media sizes.
*/
function sortMediaSizes(capabilities: Cdd): Cdd {
if (!capabilities.printer) {
return capabilities;
}
const mediaSize = capabilities.printer.media_size;
if (!mediaSize) {
return capabilities;
}
// For the standard sizes, separate into categories, as seen in the Cloud
// Print CDD guide:
// - North American
// - Chinese
// - ISO
// - Japanese
// - Other metric
// Otherwise, assume they are custom sizes.
const categoryStandardNA: MediaSizeOption[] = [];
const categoryStandardCN: MediaSizeOption[] = [];
const categoryStandardISO: MediaSizeOption[] = [];
const categoryStandardJP: MediaSizeOption[] = [];
const categoryStandardMisc: MediaSizeOption[] = [];
const categoryCustom: MediaSizeOption[] = [];
for (let i = 0, media; (media = mediaSize.option[i]); i++) {
const name: string = media.name || 'CUSTOM';
let category: MediaSizeOption[];
if (name.startsWith('NA_')) {
category = categoryStandardNA;
} else if (
name.startsWith('PRC_') || name.startsWith('ROC_') ||
name === 'OM_DAI_PA_KAI' || name === 'OM_JUURO_KU_KAI' ||
name === 'OM_PA_KAI') {
category = categoryStandardCN;
} else if (name.startsWith('ISO_')) {
category = categoryStandardISO;
} else if (name.startsWith('JIS_') || name.startsWith('JPN_')) {
category = categoryStandardJP;
} else if (name.startsWith('OM_')) {
category = categoryStandardMisc;
} else {
assert(name === 'CUSTOM', 'Unknown media size. Assuming custom');
category = categoryCustom;
}
category.push(media);
}
// For each category, sort by name.
categoryStandardNA.sort(compareMediaNames);
categoryStandardCN.sort(compareMediaNames);
categoryStandardISO.sort(compareMediaNames);
categoryStandardJP.sort(compareMediaNames);
categoryStandardMisc.sort(compareMediaNames);
categoryCustom.sort(compareMediaNames);
// Then put it all back together.
mediaSize.option = categoryStandardNA;
mediaSize.option.push(
...categoryStandardCN, ...categoryStandardISO, ...categoryStandardJP,
...categoryStandardMisc, ...categoryCustom);
return capabilities;
}
/**
* Event types dispatched by the destination store.
* @enum {string}
*/
export enum DestinationStoreEventType {
DESTINATION_SEARCH_DONE = 'DestinationStore.DESTINATION_SEARCH_DONE',
DESTINATION_SELECT = 'DestinationStore.DESTINATION_SELECT',
DESTINATIONS_INSERTED = 'DestinationStore.DESTINATIONS_INSERTED',
ERROR = 'DestinationStore.ERROR',
SELECTED_DESTINATION_CAPABILITIES_READY = 'DestinationStore' +
'.SELECTED_DESTINATION_CAPABILITIES_READY',
// <if expr="is_chromeos">
DESTINATION_EULA_READY = 'DestinationStore.DESTINATION_EULA_READY',
DESTINATION_PRINTER_STATUS_UPDATE =
'DestinationStore.DESTINATION_PRINTER_STATUS_UPDATE',
// </if>
}
export class DestinationStore extends EventTarget {
/**
* Whether the destination store will auto select the destination that
* matches this set of parameters.
*/
private autoSelectMatchingDestination_: DestinationMatch|null = null;
/**
* Cache used for constant lookup of destinations by key.
*/
private destinationMap_: Map<string, Destination> = new Map();
/**
* Internal backing store for the data store.
*/
private destinations_: Destination[] = [];
/**
* Whether a search for destinations is in progress for each type of
* printer.
*/
private destinationSearchStatus_:
Map<PrinterType, DestinationStorePrinterSearchStatus>;
private initialDestinationSelected_: boolean = false;
/**
* Used to fetch local print destinations.
*/
private nativeLayer_: NativeLayer = NativeLayerImpl.getInstance();
// <if expr="is_chromeos">
/**
* Used to fetch information about Chrome OS local print destinations.
*/
private nativeLayerCros_: NativeLayerCros = NativeLayerCrosImpl.getInstance();
// </if>
/**
* Whether PDF printer is enabled. It's disabled, for example, in App
* Kiosk mode or when PDF printing is disallowed by policy.
*/
private pdfPrinterEnabled_: boolean = false;
private recentDestinationKeys_: string[] = [];
/**
* Currently selected destination.
*/
private selectedDestination_: Destination|null = null;
/**
* Key of the system default destination.
*/
private systemDefaultDestinationKey_: string = '';
/**
* Event tracker used to track event listeners of the destination store.
*/
private tracker_: EventTracker = new EventTracker();
private typesToSearch_: Set<PrinterType> = new Set();
/**
* Whether to default to the system default printer instead of the most
* recent destination.
*/
private useSystemDefaultAsDefault_: boolean;
/**
* A data store that stores destinations and dispatches events when the
* data store changes.
* @param addListenerCallback Function to call to add Web UI listeners in
* DestinationStore constructor.
*/
constructor(
addListenerCallback:
(eventName: string, listener: (p1: any, p2?: any) => void) => void) {
super();
this.destinationSearchStatus_ = new Map([
[
PrinterType.EXTENSION_PRINTER,
DestinationStorePrinterSearchStatus.START,
],
[PrinterType.LOCAL_PRINTER, DestinationStorePrinterSearchStatus.START],
]);
this.useSystemDefaultAsDefault_ =
loadTimeData.getBoolean('useSystemDefaultPrinter');
addListenerCallback(
'printers-added',
(type: PrinterType,
printers: LocalDestinationInfo[]|ExtensionDestinationInfo[]) =>
this.onPrintersAdded_(type, printers));
// <if expr="is_chromeos">
addListenerCallback(
'local-printers-updated',
(printers: LocalDestinationInfo[]) =>
this.onLocalPrintersUpdated_(printers));
// </if>
}
/**
* @return List of destinations
*/
destinations(): Destination[] {
return this.destinations_.slice();
}
/**
* @return Whether a search for print destinations is in progress.
*/
get isPrintDestinationSearchInProgress(): boolean {
return Array.from(this.destinationSearchStatus_.values())
.some(el => el === DestinationStorePrinterSearchStatus.SEARCHING);
}
/**
* @return The currently selected destination or null if none is selected.
*/
get selectedDestination(): Destination|null {
return this.selectedDestination_;
}
private getPrinterTypeForRecentDestination_(destination: RecentDestination):
PrinterType {
if (isPdfPrinter(destination.id)) {
return PrinterType.PDF_PRINTER;
}
if (destination.origin === DestinationOrigin.LOCAL ||
destination.origin === DestinationOrigin.CROS) {
return PrinterType.LOCAL_PRINTER;
}
assert(destination.origin === DestinationOrigin.EXTENSION);
return PrinterType.EXTENSION_PRINTER;
}
/**
* Initializes the destination store. Sets the initially selected
* destination. If any inserted destinations match this ID, that destination
* will be automatically selected.
* @param pdfPrinterDisabled Whether the PDF print destination is
* disabled in print preview.
* @param saveToDriveDisabled Whether the 'Save to Google Drive' destination
* is disabled in print preview. Only used on Chrome OS.
* @param systemDefaultDestinationId ID of the system default
* destination.
* @param serializedDefaultDestinationSelectionRulesStr Serialized
* default destination selection rules.
* @param recentDestinations The recent print destinations.
*/
init(
pdfPrinterDisabled: boolean,
// <if expr="is_chromeos">
saveToDriveDisabled: boolean,
// </if>
// <if expr="not is_chromeos">
_saveToDriveDisabled: boolean,
// </if>
systemDefaultDestinationId: string,
serializedDefaultDestinationSelectionRulesStr: string|null,
recentDestinations: RecentDestination[]) {
if (systemDefaultDestinationId) {
const systemDefaultVirtual = isPdfPrinter(systemDefaultDestinationId);
const systemDefaultType = systemDefaultVirtual ?
PrinterType.PDF_PRINTER :
PrinterType.LOCAL_PRINTER;
// <if expr="not is_chromeos">
const systemDefaultOrigin = DestinationOrigin.LOCAL;
// </if>
// <if expr="is_chromeos">
const systemDefaultOrigin = systemDefaultVirtual ?
DestinationOrigin.LOCAL :
DestinationOrigin.CROS;
// </if>
this.systemDefaultDestinationKey_ =
createDestinationKey(systemDefaultDestinationId, systemDefaultOrigin);
this.typesToSearch_.add(systemDefaultType);
}
this.recentDestinationKeys_ = recentDestinations.map(
destination => createRecentDestinationKey(destination));
for (const recent of recentDestinations) {
this.typesToSearch_.add(this.getPrinterTypeForRecentDestination_(recent));
}
this.autoSelectMatchingDestination_ = this.convertToDestinationMatch_(
serializedDefaultDestinationSelectionRulesStr);
if (this.autoSelectMatchingDestination_) {
this.typesToSearch_.add(PrinterType.EXTENSION_PRINTER);
this.typesToSearch_.add(PrinterType.LOCAL_PRINTER);
}
this.pdfPrinterEnabled_ = !pdfPrinterDisabled;
this.createLocalPdfPrintDestination_();
// <if expr="is_chromeos">
if (!saveToDriveDisabled) {
this.createLocalDrivePrintDestination_();
}
// </if>
// Nothing recent, no system default ==> try to get a fallback printer as
// destinationsInserted_ may never be called.
if (this.typesToSearch_.size === 0) {
this.tryToSelectInitialDestination_();
// <if expr="is_chromeos">
// Start observing local printers if there is no attempt to load
// destinations.
this.observeLocalPrinters_();
// </if>
return;
}
for (const printerType of this.typesToSearch_) {
this.startLoadDestinations_(printerType);
}
// Start a 10s timeout so that we never hang forever.
window.setTimeout(() => {
this.tryToSelectInitialDestination_(true);
}, 10000);
}
/**
* @param timeoutExpired Whether the select timeout is expired.
* Defaults to false.
*/
private tryToSelectInitialDestination_(timeoutExpired: boolean = false) {
if (this.initialDestinationSelected_) {
return;
}
const success = this.selectInitialDestination_(timeoutExpired);
if (!success && !this.isPrintDestinationSearchInProgress &&
this.typesToSearch_.size === 0) {
// No destinations
this.dispatchEvent(new CustomEvent(
DestinationStoreEventType.ERROR,
{detail: DestinationErrorType.NO_DESTINATIONS}));
}
this.initialDestinationSelected_ = success;
}
selectDefaultDestination() {
if (this.tryToSelectDestinationByKey_(this.systemDefaultDestinationKey_)) {
return;
}
this.selectFinalFallbackDestination_();
}
/**
* Called when destinations are added to the store when the initial
* destination has not yet been set. Selects the initial destination based on
* relevant policies, recent printers, and system default.
* @param timeoutExpired Whether the initial timeout has expired.
* @return Whether an initial destination was successfully selected.
*/
private selectInitialDestination_(timeoutExpired: boolean): boolean {
const searchInProgress = this.typesToSearch_.size !== 0 && !timeoutExpired;
// System default printer policy takes priority.
if (this.useSystemDefaultAsDefault_) {
if (this.tryToSelectDestinationByKey_(
this.systemDefaultDestinationKey_)) {
return true;
}
// If search is still in progress, wait. The printer might come in a later
// batch of destinations.
if (searchInProgress) {
return false;
}
}
// Check recent destinations. If all the printers have loaded, check for all
// of them. Otherwise, just look at the most recent.
for (const key of this.recentDestinationKeys_) {
if (this.tryToSelectDestinationByKey_(key)) {
return true;
} else if (searchInProgress) {
return false;
}
}
// Try the default destination rules, if they exist.
if (this.autoSelectMatchingDestination_) {
for (const destination of this.destinations_) {
if (this.autoSelectMatchingDestination_.match(destination)) {
this.selectDestination(destination);
return true;
}
}
// If search is still in progress, wait for other possible matching
// printers.
if (searchInProgress) {
return false;
}
}
// If there either aren't any recent printers or rules, or destinations are
// all loaded and none could be found, try the system default.
if (this.tryToSelectDestinationByKey_(this.systemDefaultDestinationKey_)) {
return true;
}
if (searchInProgress) {
return false;
}
// Everything's loaded, but we couldn't find either the system default, a
// match for the selection rules, or a recent printer. Fallback to Save
// as PDF, or the first printer to load (if in kiosk mode).
if (this.selectFinalFallbackDestination_()) {
return true;
}
return false;
}
/**
* @param key The destination key to try to select.
* @return Whether the destination was found and selected.
*/
private tryToSelectDestinationByKey_(key: string): boolean {
const candidate = this.destinationMap_.get(key);
if (candidate) {
this.selectDestination(candidate);
return true;
}
return false;
}
/** Removes all events being tracked from the tracker. */
resetTracker() {
this.tracker_.removeAll();
}
// <if expr="is_chromeos">
/**
* Attempts to find the EULA URL of the the destination ID.
*/
fetchEulaUrl(destinationId: string) {
this.nativeLayerCros_.getEulaUrl(destinationId).then(response => {
// Check that the currently selected destination ID still matches the
// destination ID we used to fetch the EULA URL.
if (this.selectedDestination_ &&
destinationId === this.selectedDestination_.id) {
this.dispatchEvent(new CustomEvent(
DestinationStoreEventType.DESTINATION_EULA_READY,
{detail: response}));
}
});
}
/**
* Reloads all local printers.
*/
reloadLocalPrinters(): Promise<void> {
return this.nativeLayer_.getPrinters(PrinterType.LOCAL_PRINTER);
}
// </if>
/**
* @return Creates rules matching previously selected destination.
*/
private convertToDestinationMatch_(
serializedDefaultDestinationSelectionRulesStr: (string|null)):
(DestinationMatch|null) {
let matchRules = null;
try {
if (serializedDefaultDestinationSelectionRulesStr) {
matchRules = JSON.parse(serializedDefaultDestinationSelectionRulesStr);
}
} catch (e) {
console.warn('Failed to parse defaultDestinationSelectionRules: ' + e);
}
if (!matchRules) {
return null;
}
const isLocal = !matchRules.kind || matchRules.kind === 'local';
if (!isLocal) {
console.warn('Unsupported type: "' + matchRules.kind + '"');
return null;
}
let idRegExp = null;
try {
if (matchRules.idPattern) {
idRegExp = new RegExp(matchRules.idPattern || '.*');
}
} catch (e) {
console.warn('Failed to parse regexp for "id": ' + e);
}
let displayNameRegExp = null;
try {
if (matchRules.namePattern) {
displayNameRegExp = new RegExp(matchRules.namePattern || '.*');
}
} catch (e) {
console.warn('Failed to parse regexp for "name": ' + e);
}
return new DestinationMatch(idRegExp, displayNameRegExp);
}
/**
* This function is only invoked when the user selects a new destination via
* the UI. Programmatic selection of a destination should not use this
* function.
* @param Key identifying the destination to select
*/
selectDestinationByKey(key: string) {
const success = this.tryToSelectDestinationByKey_(key);
assert(success);
// <if expr="is_chromeos">
if (success && this.selectedDestination_ &&
this.selectedDestination_.type !== PrinterType.PDF_PRINTER) {
this.selectedDestination_.printerManuallySelected = true;
}
// </if>
}
/**
* @param destination Destination to select.
* @param refreshDestination Set to true to allow the currently selected
* destination to be re-selected.
*/
selectDestination(
destination: Destination, refreshDestination: boolean = false) {
// <if expr="not is_chromeos">
assert(!refreshDestination, 'refreshDestination for CrOS only');
if (destination === this.selectedDestination_) {
return;
}
// </if>
// <if expr="is_chromeos">
// Do not re-select the same destination unless explicitly requesting it to
// refetch the capabilities and reload the preview.
if (destination === this.selectedDestination_ && !refreshDestination) {
return;
}
// </if>
if (destination === null) {
this.selectedDestination_ = null;
this.dispatchEvent(
new CustomEvent(DestinationStoreEventType.DESTINATION_SELECT));
return;
}
// <if expr="is_chromeos">
assert(
!destination.isProvisional, 'Unable to select provisonal destinations');
// </if>
// Update and persist selected destination.
this.selectedDestination_ = destination;
// Notify about selected destination change.
this.dispatchEvent(
new CustomEvent(DestinationStoreEventType.DESTINATION_SELECT));
// Request destination capabilities from backend, since they are not
// known yet.
if (destination.capabilities === null) {
this.nativeLayer_.getPrinterCapabilities(destination.id, destination.type)
.then(
(caps) => this.onCapabilitiesSet_(
destination.origin, destination.id, caps),
() => this.onGetCapabilitiesFail_(
destination.origin, destination.id));
} else {
this.sendSelectedDestinationUpdateEvent_();
}
}
// <if expr="is_chromeos">
/**
* Attempt to resolve the capabilities for a Chrome OS printer.
*/
resolveCrosDestination(destination: Destination):
Promise<PrinterSetupResponse> {
assert(destination.origin === DestinationOrigin.CROS);
return this.nativeLayerCros_.setupPrinter(destination.id);
}
/**
* Attempts to resolve a provisional destination.
* @param Provisional destination that should be resolved.
*/
resolveProvisionalDestination(destination: Destination):
Promise<Destination|null> {
assert(
destination.provisionalType ===
DestinationProvisionalType.NEEDS_USB_PERMISSION,
'Provisional type cannot be resolved.');
return this.nativeLayerCros_.grantExtensionPrinterAccess(destination.id)
.then(
destinationInfo => {
/**
* Removes the destination from the store and replaces it with a
* destination created from the resolved destination properties,
* if any are reported. Then returns the new destination.
*/
this.removeProvisionalDestination_(destination.id);
const parsedDestination =
parseExtensionDestination(destinationInfo);
this.insertIntoStore_(parsedDestination);
return parsedDestination;
},
() => {
/**
* The provisional destination is removed from the store and
* null is returned.
*/
this.removeProvisionalDestination_(destination.id);
return null;
});
}
// </if>
/**
* Selects the Save as PDF fallback if it is available. If not, selects the
* first destination if it exists.
* @return Whether a final destination could be found.
*/
private selectFinalFallbackDestination_(): boolean {
// Save as PDF should always exist if it is enabled.
if (this.pdfPrinterEnabled_) {
const destination = this.destinationMap_.get(PDF_DESTINATION_KEY);
assert(destination);
this.selectDestination(destination);
return true;
}
// Try selecting the first destination if there is at least one
// destination already loaded.
if (this.destinations_.length > 0) {
this.selectDestination(this.destinations_[0]);
return true;
}
// Trigger a load of all destination types, to try to select the first one.
this.startLoadAllDestinations();
return false;
}
/**
* Initiates loading of destinations.
* @param type The type of destinations to load.
*/
private startLoadDestinations_(type: PrinterType) {
if (this.destinationSearchStatus_.get(type) ===
DestinationStorePrinterSearchStatus.DONE) {
return;
}
this.destinationSearchStatus_.set(
type, DestinationStorePrinterSearchStatus.SEARCHING);
this.nativeLayer_.getPrinters(type).then(
() => this.onDestinationSearchDone_(type));
}
/** Initiates loading of all known destination types. */
startLoadAllDestinations() {
// Printer types that need to be retrieved from the handler.
const types = [
PrinterType.EXTENSION_PRINTER,
PrinterType.LOCAL_PRINTER,
];
for (const printerType of types) {
this.startLoadDestinations_(printerType);
}
}
/**
* @return The destination matching the key, if it exists.
*/
getDestinationByKey(key: string): Destination|undefined {
return this.destinationMap_.get(key);
}
// <if expr="is_chromeos">
/**
* Removes the provisional destination with ID |provisionalId| from
* |destinationMap_| and |destinations_|.
*/
private removeProvisionalDestination_(provisionalId: string) {
this.destinations_ = this.destinations_.filter(el => {
if (el.id === provisionalId) {
this.destinationMap_.delete(el.key);
return false;
}
return true;
});
}
// </if>
/**
* Inserts {@code destination} to the data store and dispatches a
* DESTINATIONS_INSERTED event.
*/
private insertDestination_(destination: Destination) {
if (this.insertIntoStore_(destination)) {
this.destinationsInserted_();
}
}
/**
* Inserts multiple {@code destinations} to the data store and dispatches
* single DESTINATIONS_INSERTED event.
*/
private insertDestinations_(destinations: Array<Destination|null>) {
let inserted = false;
destinations.forEach(destination => {
if (destination) {
inserted = this.insertIntoStore_(destination!) || inserted;
}
});
if (inserted) {
this.destinationsInserted_();
}
}
/**
* Dispatches DESTINATIONS_INSERTED event. In auto select mode, tries to
* update selected destination to match
* {@code autoSelectMatchingDestination_}.
*/
private destinationsInserted_() {
this.dispatchEvent(
new CustomEvent(DestinationStoreEventType.DESTINATIONS_INSERTED));
this.tryToSelectInitialDestination_();
}
/**
* Sends SELECTED_DESTINATION_CAPABILITIES_READY event if the destination
* is supported, or ERROR otherwise of with error type UNSUPPORTED.
*/
private sendSelectedDestinationUpdateEvent_() {
this.dispatchEvent(new CustomEvent(
DestinationStoreEventType.SELECTED_DESTINATION_CAPABILITIES_READY));
}
/**
* Updates an existing print destination with capabilities and display name
* information. If the destination doesn't already exist, it will be added.
*/
private updateDestination_(destination: Destination) {
assert(destination.constructor !== Array, 'Single printer expected');
assert(destination.capabilities);
destination.capabilities = localizeCapabilities(destination.capabilities);
if (destination.type !== PrinterType.LOCAL_PRINTER) {
destination.capabilities = sortMediaSizes(destination.capabilities);
}
const existingDestination = this.destinationMap_.get(destination.key);
if (existingDestination !== undefined) {
existingDestination.capabilities = destination.capabilities;
} else {
this.insertDestination_(destination);
}
if (this.selectedDestination_ &&
(existingDestination === this.selectedDestination_ ||
destination === this.selectedDestination_)) {
this.sendSelectedDestinationUpdateEvent_();
}
}
/**
* Inserts a destination into the store without dispatching any events.
* @return Whether the inserted destination was not already in the store.
*/
private insertIntoStore_(destination: Destination): boolean {
const key = destination.key;
const existingDestination = this.destinationMap_.get(key);
if (existingDestination === undefined) {
this.destinations_.push(destination);
this.destinationMap_.set(key, destination);
return true;
}
return false;
}
/**
* Creates a local PDF print destination.
*/
private createLocalPdfPrintDestination_() {
if (this.pdfPrinterEnabled_) {
this.insertDestination_(new Destination(
GooglePromotedDestinationId.SAVE_AS_PDF, DestinationOrigin.LOCAL,
loadTimeData.getString('printToPDF')));
}
if (this.typesToSearch_.has(PrinterType.PDF_PRINTER)) {
this.typesToSearch_.delete(PrinterType.PDF_PRINTER);
}
}
// <if expr="is_chromeos">
/**
* Creates a local Drive print destination.
*/
private createLocalDrivePrintDestination_() {
this.insertDestination_(new Destination(
GooglePromotedDestinationId.SAVE_TO_DRIVE_CROS, DestinationOrigin.LOCAL,
loadTimeData.getString('printToGoogleDrive')));
}
// </if>
/**
* Called when destination search is complete for some type of printer.
* @param type The type of printers that are done being retrieved.
*/
private onDestinationSearchDone_(type: PrinterType) {
this.destinationSearchStatus_.set(
type, DestinationStorePrinterSearchStatus.DONE);
this.dispatchEvent(
new CustomEvent(DestinationStoreEventType.DESTINATION_SEARCH_DONE));
if (this.typesToSearch_.has(type)) {
this.typesToSearch_.delete(type);
this.tryToSelectInitialDestination_();
} else if (this.typesToSearch_.size === 0) {
this.tryToSelectInitialDestination_();
}
// <if expr="is_chromeos">
this.observeLocalPrinters_();
// </if>
}
/**
* Called when the native layer retrieves the capabilities for the selected
* local destination. Updates the destination with new capabilities if the
* destination already exists, otherwise it creates a new destination and
* then updates its capabilities.
* @param origin The origin of the print destination.
* @param id The id of the print destination.
* @param settingsInfo Contains the capabilities of the print destination,
* and information about the destination except in the case of extension
* printers.
*/
private onCapabilitiesSet_(
origin: DestinationOrigin, id: string,
settingsInfo: CapabilitiesResponse) {
let dest = null;
const key = createDestinationKey(id, origin);
dest = this.destinationMap_.get(key);
if (!dest) {
// Ignore unrecognized extension printers
if (!settingsInfo.printer) {
assert(origin === DestinationOrigin.EXTENSION);
return;
}
assert(settingsInfo.printer);
// PDF, CROS, and LOCAL printers all get parsed the same way.
const typeToParse = origin === DestinationOrigin.EXTENSION ?
PrinterType.EXTENSION_PRINTER :
PrinterType.LOCAL_PRINTER;
dest = parseDestination(typeToParse, settingsInfo.printer);
}
if (dest) {
if (dest.type !== PrinterType.EXTENSION_PRINTER && dest.capabilities) {
// If capabilities are already set for this destination ignore new
// results. This prevents custom margins from being cleared as long
// as the user does not change to a new non-recent destination.
return;
}
dest.capabilities = settingsInfo.capabilities;
this.updateDestination_(dest);
// <if expr="is_chromeos">
// Start the fetch for the PPD EULA URL.
this.fetchEulaUrl(dest.id);
// </if>
}
}
/**
* Called when a request to get a local destination's print capabilities
* fails. If the destination is the initial destination, auto-select another
* destination instead.
* @param _origin The origin type of the failed destination.
* @param destinationId The destination ID that failed.
*/
private onGetCapabilitiesFail_(
_origin: DestinationOrigin, destinationId: string) {
console.warn(
'Failed to get print capabilities for printer ' + destinationId);
if (this.selectedDestination_ &&
this.selectedDestination_.id === destinationId) {
this.dispatchEvent(new CustomEvent(
DestinationStoreEventType.ERROR,
{detail: DestinationErrorType.INVALID}));
}
}
/**
* Called when a printer or printers are detected after sending getPrinters
* from the native layer.
* @param type The type of printer(s) added.
* @param printers Information about the printers that have been retrieved.
*/
private onPrintersAdded_(
type: PrinterType,
printers: LocalDestinationInfo[]|ExtensionDestinationInfo[]) {
this.insertDestinations_(printers.map(
(printer: LocalDestinationInfo|ExtensionDestinationInfo) =>
parseDestination(type, printer)));
}
// <if expr="is_chromeos">
private observeLocalPrinters_() {
this.nativeLayerCros_.observeLocalPrinters().then(
(printers: LocalDestinationInfo[]) =>
this.onLocalPrintersUpdated_(printers));
}
/**
* Inserts any new printers retrieved from the 'local-printers-updated' event.
* @param printerType The type of printer(s) added.
* @param printers Information about the printers that have been retrieved.
*/
private onLocalPrintersUpdated_(printers: LocalDestinationInfo[]) {
if (!printers) {
return;
}
// The logic in insertDestinations_() ensures only new destinations are
// added to the store.
this.insertDestinations_(printers.map(
printer => parseDestination(PrinterType.LOCAL_PRINTER, printer)));
// Parse the printer status from the LocalDestinationInfo object.
for (const printer of printers) {
this.parsePrinterStatus(printer);
}
}
// Updates the printer status for an existing destination then fires an event
// for updating printer status icons and text.
private parsePrinterStatus(destinationInfo: LocalDestinationInfo): void {
const printerStatus = destinationInfo.printerStatus;
if (!printerStatus || !printerStatus.printerId) {
return;
}
const destinationKey = createDestinationKey(
destinationInfo.deviceName, DestinationOrigin.CROS);
const existingDestination = this.destinationMap_.get(destinationKey);
if (existingDestination === undefined) {
return;
}
// `nowOnline` captures the event where a previously offline printer
// becomes reachable. This will be used to trigger the destination to
// reload its preview.
const previousStatusReason = existingDestination.printerStatusReason;
const nextStatusReason = getStatusReasonFromPrinterStatus(printerStatus);
const nowOnline =
previousStatusReason === PrinterStatusReason.PRINTER_UNREACHABLE &&
(nextStatusReason !== PrinterStatusReason.PRINTER_UNREACHABLE &&
nextStatusReason !== PrinterStatusReason.UNKNOWN_REASON);
existingDestination.printerStatusReason = nextStatusReason;
this.dispatchEvent(new CustomEvent(
DestinationStoreEventType.DESTINATION_PRINTER_STATUS_UPDATE, {
detail: {
destinationKey: destinationKey,
nowOnline: nowOnline,
},
}));
}
// </if>
}
/**
* Human readable names for media sizes in the cloud print CDD.
* https://developers.google.com/cloud-print/docs/cdd
*/
const MEDIA_DISPLAY_NAMES_: {[key: string]: string} = {
'ISO_2A0': '2A0',
'ISO_A0': 'A0',
'ISO_A0X3': 'A0x3',
'ISO_A1': 'A1',
'ISO_A10': 'A10',
'ISO_A1X3': 'A1x3',
'ISO_A1X4': 'A1x4',
'ISO_A2': 'A2',
'ISO_A2X3': 'A2x3',
'ISO_A2X4': 'A2x4',
'ISO_A2X5': 'A2x5',
'ISO_A3': 'A3',
'ISO_A3X3': 'A3x3',
'ISO_A3X4': 'A3x4',
'ISO_A3X5': 'A3x5',
'ISO_A3X6': 'A3x6',
'ISO_A3X7': 'A3x7',
'ISO_A3_EXTRA': 'A3 Extra',
'ISO_A4': 'A4',
'ISO_A4X3': 'A4x3',
'ISO_A4X4': 'A4x4',
'ISO_A4X5': 'A4x5',
'ISO_A4X6': 'A4x6',
'ISO_A4X7': 'A4x7',
'ISO_A4X8': 'A4x8',
'ISO_A4X9': 'A4x9',
'ISO_A4_EXTRA': 'A4 Extra',
'ISO_A4_TAB': 'A4 Tab',
'ISO_A5': 'A5',
'ISO_A5_EXTRA': 'A5 Extra',
'ISO_A6': 'A6',
'ISO_A7': 'A7',
'ISO_A8': 'A8',
'ISO_A9': 'A9',
'ISO_B0': 'B0',
'ISO_B1': 'B1',
'ISO_B10': 'B10',
'ISO_B2': 'B2',
'ISO_B3': 'B3',
'ISO_B4': 'B4',
'ISO_B5': 'B5',
'ISO_B5_EXTRA': 'B5 Extra',
'ISO_B6': 'B6',
'ISO_B6C4': 'B6C4',
'ISO_B7': 'B7',
'ISO_B8': 'B8',
'ISO_B9': 'B9',
'ISO_C0': 'C0',
'ISO_C1': 'C1',
'ISO_C10': 'C10',
'ISO_C2': 'C2',
'ISO_C3': 'C3',
'ISO_C4': 'C4',
'ISO_C5': 'C5',
'ISO_C6': 'C6',
'ISO_C6C5': 'C6C5',
'ISO_C7': 'C7',
'ISO_C7C6': 'C7C6',
'ISO_C8': 'C8',
'ISO_C9': 'C9',
'ISO_DL': 'Envelope DL',
'ISO_RA0': 'RA0',
'ISO_RA1': 'RA1',
'ISO_RA2': 'RA2',
'ISO_SRA0': 'SRA0',
'ISO_SRA1': 'SRA1',
'ISO_SRA2': 'SRA2',
'JIS_B0': 'B0 (JIS)',
'JIS_B1': 'B1 (JIS)',
'JIS_B10': 'B10 (JIS)',
'JIS_B2': 'B2 (JIS)',
'JIS_B3': 'B3 (JIS)',
'JIS_B4': 'B4 (JIS)',
'JIS_B5': 'B5 (JIS)',
'JIS_B6': 'B6 (JIS)',
'JIS_B7': 'B7 (JIS)',
'JIS_B8': 'B8 (JIS)',
'JIS_B9': 'B9 (JIS)',
'JIS_EXEC': 'Executive (JIS)',
'JPN_CHOU2': 'Choukei 2',
'JPN_CHOU3': 'Choukei 3',
'JPN_CHOU4': 'Choukei 4',
'JPN_HAGAKI': 'Hagaki',
'JPN_KAHU': 'Kahu Envelope',
'JPN_KAKU2': 'Kaku 2',
'JPN_OUFUKU': 'Oufuku Hagaki',
'JPN_YOU4': 'You 4',
'NA_10X11': '10x11',
'NA_10X13': '10x13',
'NA_10X14': '10x14',
'NA_10X15': '10x15',
'NA_11X12': '11x12',
'NA_11X15': '11x15',
'NA_12X19': '12x19',
'NA_5X7': '5x7',
'NA_6X9': '6x9',
'NA_7X9': '7x9',
'NA_9X11': '9x11',
'NA_A2': 'A2',
'NA_ARCH_A': 'Arch A',
'NA_ARCH_B': 'Arch B',
'NA_ARCH_C': 'Arch C',
'NA_ARCH_D': 'Arch D',
'NA_ARCH_E': 'Arch E',
'NA_ASME_F': 'ASME F',
'NA_B_PLUS': 'B-plus',
'NA_C': 'C',
'NA_C5': 'C5',
'NA_D': 'D',
'NA_E': 'E',
'NA_EDP': 'EDP',
'NA_EUR_EDP': 'European EDP',
'NA_EXECUTIVE': 'Executive',
'NA_F': 'F',
'NA_FANFOLD_EUR': 'FanFold European',
'NA_FANFOLD_US': 'FanFold US',
'NA_FOOLSCAP': 'FanFold German Legal',
'NA_GOVT_LEGAL': '8x13',
'NA_GOVT_LETTER': '8x10',
'NA_INDEX_3X5': 'Index 3x5',
'NA_INDEX_4X6': 'Index 4x6',
'NA_INDEX_4X6_EXT': 'Index 4x6 ext',
'NA_INDEX_5X8': '5x8',
'NA_INVOICE': 'Invoice',
'NA_LEDGER': 'Tabloid', // Ledger in portrait is called Tabloid.
'NA_LEGAL': 'Legal',
'NA_LEGAL_EXTRA': 'Legal extra',
'NA_LETTER': 'Letter',
'NA_LETTER_EXTRA': 'Letter extra',
'NA_LETTER_PLUS': 'Letter plus',
'NA_MONARCH': 'Monarch',
'NA_NUMBER_10': 'Envelope #10',
'NA_NUMBER_11': 'Envelope #11',
'NA_NUMBER_12': 'Envelope #12',
'NA_NUMBER_14': 'Envelope #14',
'NA_NUMBER_9': 'Envelope #9',
'NA_PERSONAL': 'Personal',
'NA_QUARTO': 'Quarto',
'NA_SUPER_A': 'Super A',
'NA_SUPER_B': 'Super B',
'NA_WIDE_FORMAT': 'Wide format',
'OM_DAI_PA_KAI': 'Dai-pa-kai',
'OM_FOLIO': 'Folio',
'OM_FOLIO_SP': 'Folio SP',
'OM_INVITE': 'Invite Envelope',
'OM_ITALIAN': 'Italian Envelope',
'OM_JUURO_KU_KAI': 'Juuro-ku-kai',
'OM_LARGE_PHOTO': 'Large photo',
'OM_OFICIO': 'Oficio',
'OM_PA_KAI': 'Pa-kai',
'OM_POSTFIX': 'Postfix Envelope',
'OM_SMALL_PHOTO': 'Small photo',
'PRC_1': 'prc1 Envelope',
'PRC_10': 'prc10 Envelope',
'PRC_16K': 'prc 16k',
'PRC_2': 'prc2 Envelope',
'PRC_3': 'prc3 Envelope',
'PRC_32K': 'prc 32k',
'PRC_4': 'prc4 Envelope',
'PRC_5': 'prc5 Envelope',
'PRC_6': 'prc6 Envelope',
'PRC_7': 'prc7 Envelope',
'PRC_8': 'prc8 Envelope',
'ROC_16K': 'ROC 16K',
'ROC_8K': 'ROC 8k',
};