Commit 1815372e authored by Gavin Williams's avatar Gavin Williams Committed by Commit Bot

Add printer status call to dropdown

- Whenever the Chrome OS print preview is opened this sends a printer
  status request for each printer in the recent destinations list. The
  status comes back from the printer and the color indicator is updated
  appropriately.

- The "dot" span for the status indicator will be replaced by an icon
  in a subsequent CL.

Bug: 1059607
Change-Id: I364f852b6be33db3b5014c04b11f14e5aca2c639
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2191976
Commit-Queue: Gavin Williams <gavinwill@chromium.org>
Reviewed-by: default avatarRebekah Potter <rbpotter@chromium.org>
Cr-Commit-Position: refs/heads/master@{#774505}
parent 96421eff
...@@ -131,6 +131,7 @@ js_library("native_layer") { ...@@ -131,6 +131,7 @@ js_library("native_layer") {
"data:destination_match", "data:destination_match",
"data:destination_policies", "data:destination_policies",
"data:measurement_system", "data:measurement_system",
"data:printer_status_cros",
"//ui/webui/resources/js:assert.m", "//ui/webui/resources/js:assert.m",
"//ui/webui/resources/js:cr.m", "//ui/webui/resources/js:cr.m",
] ]
......
...@@ -22,6 +22,7 @@ js_type_check("closure_compile_module") { ...@@ -22,6 +22,7 @@ js_type_check("closure_compile_module") {
":measurement_system", ":measurement_system",
":model", ":model",
":printable_area", ":printable_area",
":printer_status_cros",
":scaling", ":scaling",
":size", ":size",
":state", ":state",
...@@ -165,3 +166,6 @@ js_library("user_manager") { ...@@ -165,3 +166,6 @@ js_library("user_manager") {
"//ui/webui/resources/js:web_ui_listener_behavior.m", "//ui/webui/resources/js:web_ui_listener_behavior.m",
] ]
} }
js_library("printer_status_cros") {
}
// Copyright 2020 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.
/**
* These values must be kept in sync with the Reason enum in
* /chromeos/printing/cups_printer_status.h
* @enum {number}
*/
export const PrinterStatusReason = {
CONNECTING_TO_DEVICE: 0,
DEVICE_ERROR: 1,
DOOR_OPEN: 2,
LOW_ON_INK: 3,
LOW_ON_PAPER: 4,
NO_ERROR: 5,
OUT_OF_INK: 6,
OUT_OF_PAPER: 7,
OUTPUT_ALMOST_FULL: 8,
OUTPUT_FULL: 9,
PAPER_JAM: 10,
PAUSED: 11,
PRINTER_QUEUE_FULL: 12,
PRINTER_UNREACHABLE: 13,
STOPPED: 14,
TRAY_MISSING: 15,
UNKNOWN_REASON: 16,
};
/**
* These values must be kept in sync with the Severity enum in
* /chromeos/printing/cups_printer_status.h
* @enum {number}
*/
export const PrinterStatusSeverity = {
UNKOWN_SEVERITY: 0,
REPORT: 1,
WARNING: 2,
ERROR: 3,
};
/**
* A container for the results of a printer status query. A printer status query
* can return multiple error reasons. |timestamp| is set at the time of status
* creation.
*
* @typedef {{
* printerId: string,
* statusReasons: !Array<{
* reason: PrinterStatusReason,
* severity: PrinterStatusSeverity,
* }>,
* timestamp: number,
* }}
*/
export let PrinterStatus;
...@@ -9,6 +9,7 @@ import {Cdd, Destination} from './data/destination.js'; ...@@ -9,6 +9,7 @@ import {Cdd, Destination} from './data/destination.js';
import {PrinterType} from './data/destination_match.js'; import {PrinterType} from './data/destination_match.js';
// <if expr="chromeos"> // <if expr="chromeos">
import {DestinationPolicies} from './data/destination_policies.js'; import {DestinationPolicies} from './data/destination_policies.js';
import {PrinterStatus} from './data/printer_status_cros.js';
// </if> // </if>
import {MeasurementSystemUnitType} from './data/measurement_system.js'; import {MeasurementSystemUnitType} from './data/measurement_system.js';
...@@ -270,6 +271,15 @@ export class NativeLayer { ...@@ -270,6 +271,15 @@ export class NativeLayer {
*/ */
signIn(addAccount) {} signIn(addAccount) {}
// <if expr="chromeos">
/**
* Sends a request to the printer with id |printerId| for its current status.
* @param {string} printerId
* @return {!Promise<!PrinterStatus>}
*/
requestPrinterStatusUpdate(printerId) {}
// </if>
/** /**
* Notifies the metrics handler to record a histogram value. * Notifies the metrics handler to record a histogram value.
* @param {string} histogram The name of the histogram to record * @param {string} histogram The name of the histogram to record
...@@ -375,6 +385,13 @@ export class NativeLayerImpl { ...@@ -375,6 +385,13 @@ export class NativeLayerImpl {
chrome.send('signIn', [addAccount]); chrome.send('signIn', [addAccount]);
} }
// <if expr="chromeos">
/** @override */
requestPrinterStatusUpdate(printerId) {
return sendWithPromise('requestPrinterStatus', printerId);
}
// </if>
/** @override */ /** @override */
recordInHistogram(histogram, bucket, maxBucket) { recordInHistogram(histogram, bucket, maxBucket) {
chrome.send( chrome.send(
......
...@@ -10,6 +10,7 @@ export {ColorMode, createDestinationKey, Destination, DestinationCertificateStat ...@@ -10,6 +10,7 @@ export {ColorMode, createDestinationKey, Destination, DestinationCertificateStat
export {PrinterType} from './data/destination_match.js'; export {PrinterType} from './data/destination_match.js';
// <if expr="chromeos"> // <if expr="chromeos">
export {ColorModeRestriction, DuplexModeRestriction, PinModeRestriction} from './data/destination_policies.js'; export {ColorModeRestriction, DuplexModeRestriction, PinModeRestriction} from './data/destination_policies.js';
export {PrinterStatus, PrinterStatusReason, PrinterStatusSeverity} from './data/printer_status_cros.js';
// </if> // </if>
export {DestinationErrorType, DestinationStore} from './data/destination_store.js'; export {DestinationErrorType, DestinationStore} from './data/destination_store.js';
export {PageLayoutInfo} from './data/document_info.js'; export {PageLayoutInfo} from './data/document_info.js';
......
...@@ -199,6 +199,10 @@ ...@@ -199,6 +199,10 @@
file="data/destination_policies.js" file="data/destination_policies.js"
compress="false" compress="false"
type="chrome_html" /> type="chrome_html" />
<structure name="IDR_PRINT_PREVIEW_DATA_PRINTER_STATUS_CROS_JS"
file="data/printer_status_cros.js"
compress="false"
type="chrome_html" />
</if> </if>
<structure name="IDR_PRINT_PREVIEW_DATA_DESTINATION_STORE_JS" <structure name="IDR_PRINT_PREVIEW_DATA_DESTINATION_STORE_JS"
file="data/destination_store.js" file="data/destination_store.js"
......
...@@ -165,6 +165,7 @@ if (is_chromeos) { ...@@ -165,6 +165,7 @@ if (is_chromeos) {
":select_behavior", ":select_behavior",
"..:print_preview_utils", "..:print_preview_utils",
"../data:destination", "../data:destination",
"../data:printer_status_cros",
"//third_party/polymer/v3_0/components-chromium/iron-iconset-svg:iron-iconset-svg", "//third_party/polymer/v3_0/components-chromium/iron-iconset-svg:iron-iconset-svg",
"//third_party/polymer/v3_0/components-chromium/iron-meta:iron-meta", "//third_party/polymer/v3_0/components-chromium/iron-meta:iron-meta",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled", "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
......
...@@ -88,6 +88,14 @@ ...@@ -88,6 +88,14 @@
background-color: rgba(0, 0, 0, .12); background-color: rgba(0, 0, 0, .12);
outline: none; outline: none;
} }
.dot {
background-color: #bbb;
border-radius: 50%;
display: inline-block;
height: 10px;
width: 10px;
}
</style> </style>
<cr-input id="dropdownInput" on-keydown="onKeyDown_" <cr-input id="dropdownInput" on-keydown="onKeyDown_"
value="[[value.displayName]]" readonly> value="[[value.displayName]]" readonly>
...@@ -101,8 +109,9 @@ ...@@ -101,8 +109,9 @@
no-cancel-on-esc-key> no-cancel-on-esc-key>
<div slot="dropdown-content"> <div slot="dropdown-content">
<template is="dom-repeat" items="[[itemList]]"> <template is="dom-repeat" items="[[itemList]]">
<button class="list-item" on-click="onSelect_" tabindex="-1" <button id="[[item.key]]" class="list-item" on-click="onSelect_"
value="[[item.key]]"> tabindex="-1" value="[[item.key]]">
<span class="dot"></span>
[[item.displayName]] [[item.displayName]]
</button> </button>
</template> </template>
......
...@@ -22,6 +22,8 @@ import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js'; ...@@ -22,6 +22,8 @@ import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {Base, html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; import {Base, html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {Destination, DestinationOrigin, PDF_DESTINATION_KEY, RecentDestination} from '../data/destination.js'; import {Destination, DestinationOrigin, PDF_DESTINATION_KEY, RecentDestination} from '../data/destination.js';
import {PrinterStatus, PrinterStatusReason, PrinterStatusSeverity} from '../data/printer_status_cros.js';
import {NativeLayer, NativeLayerImpl} from '../native_layer.js';
import {getSelectDropdownBackground} from '../print_preview_utils.js'; import {getSelectDropdownBackground} from '../print_preview_utils.js';
import {SelectBehavior} from './select_behavior.js'; import {SelectBehavior} from './select_behavior.js';
...@@ -52,7 +54,10 @@ Polymer({ ...@@ -52,7 +54,10 @@ Polymer({
pdfPrinterDisabled: Boolean, pdfPrinterDisabled: Boolean,
/** @type {!Array<!Destination>} */ /** @type {!Array<!Destination>} */
recentDestinationList: Array, recentDestinationList: {
type: Array,
observer: 'onRecentDestinationListChanged_',
},
/** @private {string} */ /** @private {string} */
pdfDestinationKey_: { pdfDestinationKey_: {
...@@ -81,12 +86,30 @@ Polymer({ ...@@ -81,12 +86,30 @@ Polymer({
}, },
readOnly: true, readOnly: true,
}, },
/**
* The key for this map is a destination.id and the value is a
* destination.key. This map is needed to track which destinations have had
* statuses requested while also giving quick look up of destination id to
* the corresponding destination key.
* @type {!Map<string, string>}
*/
statusRequestedMap_: Map,
}, },
/** @private {!IronMetaElement} */ /** @private {!IronMetaElement} */
meta_: /** @type {!IronMetaElement} */ ( meta_: /** @type {!IronMetaElement} */ (
Base.create('iron-meta', {type: 'iconset'})), Base.create('iron-meta', {type: 'iconset'})),
/** @override */
attached() {
if (!this.printerStatusFlagEnabled_) {
return;
}
this.statusRequestedMap_ = new Map();
},
focus() { focus() {
this.$$('#dropdown').focus(); this.$$('#dropdown').focus();
}, },
...@@ -191,4 +214,102 @@ Polymer({ ...@@ -191,4 +214,102 @@ Polymer({
this.fire('selected-option-change', selectedItem.value); this.fire('selected-option-change', selectedItem.value);
}, },
/**
* Send a printer status request for any new destination in the dropdown.
* @private
*/
onRecentDestinationListChanged_() {
if (!this.printerStatusFlagEnabled_) {
return;
}
for (const destination of this.recentDestinationList) {
if (destination.origin !== DestinationOrigin.CROS ||
this.statusRequestedMap_.has(destination.id)) {
continue;
}
NativeLayerImpl.getInstance()
.requestPrinterStatusUpdate(destination.id)
.then(status => this.onPrinterStatusReceived_(status));
this.statusRequestedMap_.set(destination.id, destination.key);
}
},
/**
* Check if the printer in |printerStatus| is currently in the dropdown.
* Update its status icon if it's present.
* @param {!PrinterStatus} printerStatus
* @private
*/
onPrinterStatusReceived_(printerStatus) {
assert(this.printerStatusFlagEnabled_);
if (!printerStatus.printerId) {
return;
}
const destinationKey =
this.statusRequestedMap_.get(printerStatus.printerId);
if (!destinationKey) {
return;
}
// Regex to escape the forward slashes in destination keys.
// TODO(gavinwill): Change from circle styles to real icons.
const circle =
this.$$('#dropdown').$$(`#${destinationKey.replace(/\//g, '\\/')}`);
if (!circle) {
return;
}
const statusReason = this.getStatusReasonFromPrinterStatus_(printerStatus);
if (statusReason === PrinterStatusReason.NO_ERROR) {
circle.firstChild.style.backgroundColor = 'green';
return;
}
if (statusReason !== PrinterStatusReason.UNKNOWN_REASON) {
circle.firstChild.style.backgroundColor = 'red';
return;
}
},
/**
* A |printerStatus| can have multiple status reasons so this function's
* responsibility is to determine which status reason is most relevant to
* surface to the user. Any status reason with a severity of WARNING or ERROR
* will get highest precedence since this usually means the printer is in a
* bad state. NO_ERROR status reason is the next highest precedence so the
* printer can be shown as available whenever possible.
* @param {!PrinterStatus} printerStatus
* @return {!PrinterStatusReason} Status reason extracted from
* |printerStatus|.
* @private
*/
getStatusReasonFromPrinterStatus_(printerStatus) {
assert(this.printerStatusFlagEnabled_);
if (!printerStatus.printerId) {
return PrinterStatusReason.UNKNOWN_REASON;
}
let seenNoErrorReason = false;
for (const statusReason of printerStatus.statusReasons) {
const reason = statusReason.reason;
const severity = statusReason.severity;
if (reason !== PrinterStatusReason.UNKNOWN_REASON &&
(severity === PrinterStatusSeverity.WARNING ||
severity === PrinterStatusSeverity.ERROR)) {
return reason;
}
if (reason === PrinterStatusReason.NO_ERROR) {
seenNoErrorReason = true;
}
}
return seenNoErrorReason ? PrinterStatusReason.NO_ERROR :
PrinterStatusReason.UNKNOWN_REASON;
},
}); });
...@@ -29,7 +29,6 @@ js_type_check("closure_compile") { ...@@ -29,7 +29,6 @@ js_type_check("closure_compile") {
#":destination_list_test", #":destination_list_test",
#":destination_search_test_chromeos", #":destination_search_test_chromeos",
#":destination_search_test", #":destination_search_test",
#":destination_select_test_cros",
#":destination_select_test", #":destination_select_test",
":destination_settings_test", ":destination_settings_test",
...@@ -74,7 +73,10 @@ js_type_check("closure_compile") { ...@@ -74,7 +73,10 @@ js_type_check("closure_compile") {
] ]
if (is_chromeos) { if (is_chromeos) {
deps += [ ":destination_dropdown_cros_test" ] deps += [
":destination_dropdown_cros_test",
":destination_select_test_cros",
]
} }
} }
...@@ -204,4 +206,15 @@ if (is_chromeos) { ...@@ -204,4 +206,15 @@ if (is_chromeos) {
"//ui/webui/resources/js:assert.m", "//ui/webui/resources/js:assert.m",
] ]
} }
js_library("destination_select_test_cros") {
deps = [
":native_layer_stub",
":print_preview_test_utils",
"..:chai_assert",
"..:test_util.m",
"//chrome/browser/resources/print_preview:print_preview",
"//ui/webui/resources/js:assert.m",
]
}
} }
...@@ -26,6 +26,7 @@ export class NativeLayerStub extends TestBrowserProxy { ...@@ -26,6 +26,7 @@ export class NativeLayerStub extends TestBrowserProxy {
'getEulaUrl', 'getEulaUrl',
'hidePreview', 'hidePreview',
'print', 'print',
'requestPrinterStatusUpdate',
'saveAppState', 'saveAppState',
'setupPrinter', 'setupPrinter',
'showSystemDialog', 'showSystemDialog',
...@@ -92,6 +93,20 @@ export class NativeLayerStub extends TestBrowserProxy { ...@@ -92,6 +93,20 @@ export class NativeLayerStub extends TestBrowserProxy {
/** @private {string} license The PPD license of a destination. */ /** @private {string} license The PPD license of a destination. */
this.eulaUrl_ = ''; this.eulaUrl_ = '';
/**
* @private {!Map<string, !Object>}
* A map from printerId to PrinterStatus. Defining the value parameter as
* Object instead of PrinterStatus because the PrinterStatus type is CrOS
* specific, and this class is used by tests on all platforms.
*/
this.printerStatusMap_ = new Map();
/** @private {?PromiseResolver} */
this.multiplePrinterStatusRequestsPromise_ = null;
/** @private {number} */
this.multiplePrinterStatusRequestsCount_ = 0;
} }
/** @param {number} pageCount The number of pages in the document. */ /** @param {number} pageCount The number of pages in the document. */
...@@ -342,4 +357,42 @@ export class NativeLayerStub extends TestBrowserProxy { ...@@ -342,4 +357,42 @@ export class NativeLayerStub extends TestBrowserProxy {
setEulaUrl(eulaUrl) { setEulaUrl(eulaUrl) {
this.eulaUrl_ = eulaUrl; this.eulaUrl_ = eulaUrl;
} }
/**
* Sends a request to the printer with id |printerId| for its current status.
* @param {string} printerId
* @return {!Promise} Promise that resolves returns a printer status.
* @override
*/
requestPrinterStatusUpdate(printerId) {
this.methodCalled('requestPrinterStatusUpdate');
if (this.multiplePrinterStatusRequestsPromise_) {
this.multiplePrinterStatusRequestsCount_--;
if (this.multiplePrinterStatusRequestsCount_ === 0) {
this.multiplePrinterStatusRequestsPromise_.resolve();
this.multiplePrinterStatusRequestsPromise_ = null;
}
}
return Promise.resolve(this.printerStatusMap_.get(printerId));
}
/**
* @param {string} printerId
* @param {!Object} printerStatus
*/
addPrinterStatusToMap(printerId, printerStatus) {
this.printerStatusMap_.set(printerId, printerStatus);
}
/**
* @param {number} count The number of printer status requests to wait for.
* @return {!Promise} Promise that resolves after |count| requests.
*/
waitForMultiplePrinterStatusRequests(count) {
assert(this.multiplePrinterStatusRequestsPromise_ === null);
this.multiplePrinterStatusRequestsCount_ = count;
this.multiplePrinterStatusRequestsPromise_ = new PromiseResolver();
return this.multiplePrinterStatusRequestsPromise_.promise;
}
} }
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
GEN_INCLUDE(['//chrome/test/data/webui/polymer_browser_test_base.js']); GEN_INCLUDE(['//chrome/test/data/webui/polymer_browser_test_base.js']);
GEN('#include "chromeos/constants/chromeos_features.h"');
GEN('#include "content/public/test/browser_test.h"'); GEN('#include "content/public/test/browser_test.h"');
GEN('#include "services/network/public/cpp/features.h"'); GEN('#include "services/network/public/cpp/features.h"');
...@@ -1082,6 +1083,41 @@ TEST_F('PrintPreviewDestinationSelectTestCrOS', 'EulaIsDisplayed', function() { ...@@ -1082,6 +1083,41 @@ TEST_F('PrintPreviewDestinationSelectTestCrOS', 'EulaIsDisplayed', function() {
this.runMochaTest(destination_select_test_cros.TestNames.EulaIsDisplayed); this.runMochaTest(destination_select_test_cros.TestNames.EulaIsDisplayed);
}); });
// eslint-disable-next-line no-var
var PrintPreviewPrinterStatusTestCros = class extends PrintPreviewTest {
/** @override */
get browsePreload() {
return 'chrome://print/test_loader.html?module=print_preview/destination_select_test_cros.js';
}
/** @override */
get suiteName() {
return printer_status_test_cros.suiteName;
}
/** @override */
get featureList() {
const kPrinterStatus = ['chromeos::features::kPrinterStatus'];
const featureList = super.featureList;
featureList.enabled = featureList.enabled ?
featureList.enabled.concat(kPrinterStatus) :
kPrinterStatus;
return featureList;
}
};
TEST_F(
'PrintPreviewPrinterStatusTestCros', 'ReasonFromPrinterStatus', function() {
this.runMochaTest(
printer_status_test_cros.TestNames.ReasonFromPrinterStatus);
});
TEST_F(
'PrintPreviewPrinterStatusTestCros', 'SendStatusRequestOnce', function() {
this.runMochaTest(
printer_status_test_cros.TestNames.SendStatusRequestOnce);
});
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
var PrintPreviewDestinationDropdownCrosTest = class extends PrintPreviewTest { var PrintPreviewDestinationDropdownCrosTest = class extends PrintPreviewTest {
/** @override */ /** @override */
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment