blob: 22beb376aeefa0a78004b5febad240ea1f6599ac [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../strings.m.js';
import {assert} from 'chrome://resources/js/assert_ts.js';
// <if expr="chromeos_ash or chromeos_lacros">
import {NativeLayerCrosImpl} from '../native_layer_cros.js';
// </if>
import {Cdd, ColorCapability, ColorOption, CopiesCapability} from './cdd.js';
// <if expr="chromeos_ash or chromeos_lacros">
import {getStatusReasonFromPrinterStatus, PrinterStatus, PrinterStatusReason} from './printer_status_cros.js';
// </if>
/**
* Enumeration of the origin types for destinations.
*/
export enum DestinationOrigin {
LOCAL = 'local',
// Note: Cookies, device and privet are deprecated, but used to filter any
// legacy entries in the recent destinations, since we can't guarantee all
// such recent printers have been overridden.
COOKIES = 'cookies',
// <if expr="chromeos_ash or chromeos_lacros">
DEVICE = 'device',
// </if>
PRIVET = 'privet',
EXTENSION = 'extension',
CROS = 'chrome_os',
}
/**
* Printer types for capabilities and printer list requests.
* Must match PrinterType in printing/mojom/print.mojom
*/
export enum PrinterType {
PRIVET_PRINTER_DEPRECATED = 0,
EXTENSION_PRINTER = 1,
PDF_PRINTER = 2,
LOCAL_PRINTER = 3,
CLOUD_PRINTER_DEPRECATED = 4
}
// <if expr="chromeos_ash or chromeos_lacros">
/**
* Enumeration specifying whether a destination is provisional and the reason
* the destination is provisional.
*/
export enum DestinationProvisionalType {
// Destination is not provisional.
NONE = 'NONE',
// User has to grant USB access for the destination to its provider.
// Used for destinations with extension origin.
NEEDS_USB_PERMISSION = 'NEEDS_USB_PERMISSION',
}
// </if>
/**
* Enumeration of color modes used by Chromium.
*/
export enum ColorMode {
GRAY = 1,
COLOR = 2,
}
export interface RecentDestination {
id: string;
origin: DestinationOrigin;
capabilities: Cdd|null;
displayName: string;
extensionId: string;
extensionName: string;
icon?: string;
}
export function isPdfPrinter(id: string): boolean {
// <if expr="chromeos_ash or chromeos_lacros">
if (id === GooglePromotedDestinationId.SAVE_TO_DRIVE_CROS) {
return true;
}
// </if>
return id === GooglePromotedDestinationId.SAVE_AS_PDF;
}
/**
* Creates a |RecentDestination| to represent |destination| in the app
* state.
*/
export function makeRecentDestination(destination: Destination):
RecentDestination {
return {
id: destination.id,
origin: destination.origin,
capabilities: destination.capabilities,
displayName: destination.displayName || '',
extensionId: destination.extensionId || '',
extensionName: destination.extensionName || '',
icon: destination.icon || '',
};
}
/**
* @return key that maps to a destination with the selected |id| and |origin|.
*/
export function createDestinationKey(
id: string, origin: DestinationOrigin): string {
return `${id}/${origin}/`;
}
/**
* @return A key that maps to a destination with parameters matching
* |recentDestination|.
*/
export function createRecentDestinationKey(
recentDestination: RecentDestination): string {
return createDestinationKey(recentDestination.id, recentDestination.origin);
}
export interface DestinationOptionalParams {
isEnterprisePrinter?: boolean;
// <if expr="chromeos_ash or chromeos_lacros">
provisionalType?: DestinationProvisionalType;
// </if>
extensionId?: string;
extensionName?: string;
description?: string;
location?: string;
}
/**
* List of capability types considered color.
*/
const COLOR_TYPES: string[] = ['STANDARD_COLOR', 'CUSTOM_COLOR'];
/**
* List of capability types considered monochrome.
*/
const MONOCHROME_TYPES: string[] = ['STANDARD_MONOCHROME', 'CUSTOM_MONOCHROME'];
/**
* Print destination data object.
*/
export class Destination {
/**
* ID of the destination.
*/
private id_: string;
/**
* Origin of the destination.
*/
private origin_: DestinationOrigin;
/**
* Display name of the destination.
*/
private displayName_: string;
/**
* Print capabilities of the destination.
*/
private capabilities_: Cdd|null = null;
/**
* Whether the destination is an enterprise policy controlled printer.
*/
private isEnterprisePrinter_: boolean;
/**
* Destination location.
*/
private location_: string = '';
/**
* Printer description.
*/
private description_: string;
/**
* Extension ID for extension managed printers.
*/
private extensionId_: string;
/**
* Extension name for extension managed printers.
*/
private extensionName_: string;
// <if expr="chromeos_ash or chromeos_lacros">
/**
* Different from DestinationProvisionalType.NONE if
* the destination is provisional. Provisional destinations cannot be
* selected as they are, but have to be resolved first (i.e. extra steps
* have to be taken to get actual destination properties, which should
* replace the provisional ones). Provisional destination resolvment flow
* will be started when the user attempts to select the destination in
* search UI.
*/
private provisionalType_: DestinationProvisionalType;
/**
* EULA url for printer's PPD. Empty string indicates no provided EULA.
*/
private eulaUrl_: string = '';
/**
* Stores the printer status reason for a local Chrome OS printer.
*/
private printerStatusReason_: PrinterStatusReason|null = null;
/**
* Promise returns |key_| when the printer status request is completed.
*/
private printerStatusRequestedPromise_: Promise<string>|null = null;
/**
* True if the failed printer status request has already been retried once.
*/
private printerStatusRetrySent_: boolean = false;
/**
* The length of time to wait before retrying a printer status request.
*/
private printerStatusRetryTimerMs_: number = 3000;
// </if>
private type_: PrinterType;
constructor(
id: string, origin: DestinationOrigin, displayName: string,
params?: DestinationOptionalParams) {
this.id_ = id;
this.origin_ = origin;
this.displayName_ = displayName || '';
this.isEnterprisePrinter_ = (params && params.isEnterprisePrinter) || false;
this.description_ = (params && params.description) || '';
this.extensionId_ = (params && params.extensionId) || '';
this.extensionName_ = (params && params.extensionName) || '';
this.location_ = (params && params.location) || '';
this.type_ = this.computeType_(id, origin);
// <if expr="chromeos_ash or chromeos_lacros">
this.provisionalType_ =
(params && params.provisionalType) || DestinationProvisionalType.NONE;
assert(
this.provisionalType_ !==
DestinationProvisionalType.NEEDS_USB_PERMISSION ||
this.isExtension,
'Provisional USB destination only supprted with extension origin.');
// </if>
}
private computeType_(id: string, origin: DestinationOrigin): PrinterType {
if (isPdfPrinter(id)) {
return PrinterType.PDF_PRINTER;
}
return origin === DestinationOrigin.EXTENSION ?
PrinterType.EXTENSION_PRINTER :
PrinterType.LOCAL_PRINTER;
}
get type(): PrinterType {
return this.type_;
}
get id(): string {
return this.id_;
}
get origin(): DestinationOrigin {
return this.origin_;
}
get displayName(): string {
return this.displayName_;
}
/**
* @return Whether the destination is an extension managed printer.
*/
get isExtension(): boolean {
return this.origin_ === DestinationOrigin.EXTENSION;
}
/**
* @return Most relevant string to help user to identify this
* destination.
*/
get hint(): string {
return this.location_ || this.extensionName || this.description_;
}
/**
* @return Extension ID associated with the destination. Non-empty
* only for extension managed printers.
*/
get extensionId(): string {
return this.extensionId_;
}
/**
* @return Extension name associated with the destination.
* Non-empty only for extension managed printers.
*/
get extensionName(): string {
return this.extensionName_;
}
/** @return Print capabilities of the destination. */
get capabilities(): Cdd|null {
return this.capabilities_;
}
set capabilities(capabilities: Cdd|null) {
if (capabilities) {
this.capabilities_ = capabilities;
}
}
// <if expr="chromeos_ash or chromeos_lacros">
get eulaUrl(): string {
return this.eulaUrl_;
}
set eulaUrl(eulaUrl: string) {
this.eulaUrl_ = eulaUrl;
}
/**
* @return The printer status reason for a local Chrome OS printer.
*/
get printerStatusReason(): PrinterStatusReason|null {
return this.printerStatusReason_;
}
setPrinterStatusRetryTimeoutForTesting(timeoutMs: number) {
this.printerStatusRetryTimerMs_ = timeoutMs;
}
/**
* Requests a printer status for the destination.
* @return Promise with destination key.
*/
requestPrinterStatus(): Promise<string> {
// Requesting printer status only allowed for local CrOS printers.
if (this.origin_ !== DestinationOrigin.CROS) {
return Promise.reject();
}
// Immediately resolve promise if |printerStatusReason_| is already
// available.
if (this.printerStatusReason_) {
return Promise.resolve(this.key);
}
// Return existing promise if the printer status has already been requested.
if (this.printerStatusRequestedPromise_) {
return this.printerStatusRequestedPromise_;
}
// Request printer status then set and return the promise.
this.printerStatusRequestedPromise_ = this.requestPrinterStatusPromise_();
return this.printerStatusRequestedPromise_;
}
/**
* Requests a printer status for the destination. If the printer status comes
* back as |PRINTER_UNREACHABLE|, this function will retry and call itself
* again once before resolving the original call.
* @return Promise with destination key.
*/
private requestPrinterStatusPromise_(): Promise<string> {
return NativeLayerCrosImpl.getInstance()
.requestPrinterStatusUpdate(this.id_)
.then(status => {
if (status) {
const statusReason =
getStatusReasonFromPrinterStatus(status as PrinterStatus);
const isPrinterUnreachable =
statusReason === PrinterStatusReason.PRINTER_UNREACHABLE;
if (isPrinterUnreachable && !this.printerStatusRetrySent_) {
this.printerStatusRetrySent_ = true;
return this.printerStatusWaitForTimerPromise_();
}
this.printerStatusReason_ = statusReason;
// If this is the second printer status attempt, record the result.
if (this.printerStatusRetrySent_) {
NativeLayerCrosImpl.getInstance()
.recordPrinterStatusRetrySuccessHistogram(
!isPrinterUnreachable);
}
}
return Promise.resolve(this.key);
});
}
/**
* Pause for a set timeout then retry the printer status request.
* @return Promise with destination key.
*/
private printerStatusWaitForTimerPromise_(): Promise<string> {
return new Promise<void>((resolve, _reject) => {
setTimeout(() => {
resolve();
}, this.printerStatusRetryTimerMs_);
})
.then(() => {
return this.requestPrinterStatusPromise_();
});
}
/** @return Whether the destination is ready to be selected. */
get readyForSelection(): boolean {
return (this.origin_ !== DestinationOrigin.CROS ||
this.capabilities_ !== null) &&
!this.isProvisional;
}
get provisionalType(): DestinationProvisionalType {
return this.provisionalType_;
}
get isProvisional(): boolean {
return this.provisionalType_ !== DestinationProvisionalType.NONE;
}
// </if>
/** @return Path to the SVG for the destination's icon. */
get icon(): string {
// <if expr="chromeos_ash or chromeos_lacros">
if (this.id_ === GooglePromotedDestinationId.SAVE_TO_DRIVE_CROS) {
return 'print-preview:save-to-drive';
}
// </if>
if (this.id_ === GooglePromotedDestinationId.SAVE_AS_PDF) {
return 'cr:insert-drive-file';
}
if (this.isEnterprisePrinter) {
return 'print-preview:business';
}
return 'print-preview:print';
}
/**
* @return Properties (besides display name) to match search queries against.
*/
get extraPropertiesToMatch(): string[] {
return [this.location_, this.description_];
}
/**
* Matches a query against the destination.
* @param query Query to match against the destination.
* @return Whether the query matches this destination.
*/
matches(query: RegExp): boolean {
return !!this.displayName_.match(query) ||
!!this.extensionName_.match(query) || !!this.location_.match(query) ||
!!this.description_.match(query);
}
/**
* Whether the printer is enterprise policy controlled printer.
*/
get isEnterprisePrinter(): boolean {
return this.isEnterprisePrinter_;
}
private copiesCapability_(): CopiesCapability|null {
return this.capabilities && this.capabilities.printer &&
this.capabilities.printer.copies ?
this.capabilities.printer.copies :
null;
}
private colorCapability_(): ColorCapability|null {
return this.capabilities && this.capabilities.printer &&
this.capabilities.printer.color ?
this.capabilities.printer.color :
null;
}
/** @return Whether the printer supports copies. */
get hasCopiesCapability(): boolean {
const capability = this.copiesCapability_();
if (!capability) {
return false;
}
return capability.max ? capability.max > 1 : true;
}
/**
* @return Whether the printer supports both black and white and
* color printing.
*/
get hasColorCapability(): boolean {
const capability = this.colorCapability_();
if (!capability || !capability.option) {
return false;
}
let hasColor = false;
let hasMonochrome = false;
capability.option.forEach(option => {
assert(option.type);
hasColor = hasColor || COLOR_TYPES.includes(option.type);
hasMonochrome = hasMonochrome || MONOCHROME_TYPES.includes(option.type);
});
return hasColor && hasMonochrome;
}
/**
* @param isColor Whether to use a color printing mode.
* @return Selected color option.
*/
getSelectedColorOption(isColor: boolean): ColorOption|null {
const typesToLookFor = isColor ? COLOR_TYPES : MONOCHROME_TYPES;
const capability = this.colorCapability_();
if (!capability || !capability.option) {
return null;
}
for (let i = 0; i < typesToLookFor.length; i++) {
const matchingOptions = capability.option.filter(option => {
return option.type === typesToLookFor[i];
});
if (matchingOptions.length > 0) {
return matchingOptions[0];
}
}
return null;
}
/**
* @param isColor Whether to use a color printing mode.
* @return Native color model of the destination.
*/
getNativeColorModel(isColor: boolean): number {
// For printers without capability, native color model is ignored.
const capability = this.colorCapability_();
if (!capability || !capability.option) {
return isColor ? ColorMode.COLOR : ColorMode.GRAY;
}
const selected = this.getSelectedColorOption(isColor);
const mode = parseInt(selected ? selected.vendor_id! : '', 10);
if (isNaN(mode)) {
return isColor ? ColorMode.COLOR : ColorMode.GRAY;
}
return mode;
}
/**
* @return The default color option for the destination.
*/
get defaultColorOption(): ColorOption|null {
const capability = this.colorCapability_();
if (!capability || !capability.option) {
return null;
}
const defaultOptions = capability.option.filter(option => {
return option.is_default;
});
return defaultOptions.length !== 0 ? defaultOptions[0] : null;
}
/** @return A unique identifier for this destination. */
get key(): string {
return `${this.id_}/${this.origin_}/`;
}
}
/**
* Enumeration of Google-promoted destination IDs.
* @enum {string}
*/
export enum GooglePromotedDestinationId {
SAVE_AS_PDF = 'Save as PDF',
// <if expr="chromeos_ash or chromeos_lacros">
SAVE_TO_DRIVE_CROS = 'Save to Drive CrOS',
// </if>
}
/* Unique identifier for the Save as PDF destination */
export const PDF_DESTINATION_KEY: string =
`${GooglePromotedDestinationId.SAVE_AS_PDF}/${DestinationOrigin.LOCAL}/`;
// <if expr="chromeos_ash or chromeos_lacros">
/* Unique identifier for the Save to Drive CrOS destination */
export const SAVE_TO_DRIVE_CROS_DESTINATION_KEY: string =
`${GooglePromotedDestinationId.SAVE_TO_DRIVE_CROS}/${
DestinationOrigin.LOCAL}/`;
// </if>