import 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import 'chrome://resources/mojo/mojo/public/mojom/base/big_buffer.mojom-webui.js';
import 'chrome://resources/mojo/mojo/public/mojom/base/file_path.mojom-webui.js';
import 'chrome://resources/mojo/mojo/public/mojom/base/string16.mojom-webui.js';
import 'chrome://resources/polymer/v3_0/paper-progress/paper-progress.js';
import './firmware_shared.css.js';
import './firmware_shared_fonts.css.js';
import './firmware_update.mojom-webui.js';
import '/strings.m.js';
import type {I18nMixinInterface} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {assert} from 'chrome://resources/js/assert.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import type {DeviceRequest, DeviceRequestObserverInterface, FirmwareUpdate, InstallationProgress, InstallControllerRemote, UpdateProgressObserverInterface} from './firmware_update.mojom-webui.js';
import {DeviceRequestId, DeviceRequestKind, DeviceRequestObserverReceiver, UpdateProgressObserverReceiver, UpdateState} from './firmware_update.mojom-webui.js';
import {getTemplate} from './firmware_update_dialog.html.js';
import type {DialogContent, OpenUpdateDialogEventDetail} from './firmware_update_types.js';
import {isAppV2Enabled} from './firmware_update_utils.js';
import {getSystemUtils, getUpdateProvider} from './mojo_interface_provider.js';
const initialDialogContent: DialogContent = {
title: '',
body: '',
footer: '',
};
const initialInstallationProgress: InstallationProgress = {
percentage: 0,
state: UpdateState.kIdle,
};
* @fileoverview
* 'firmware-update-dialog' displays information related to a firmware update.
*/
const FirmwareUpdateDialogElementBase =
I18nMixin(PolymerElement) as {new (): PolymerElement & I18nMixinInterface};
export class FirmwareUpdateDialogElement extends FirmwareUpdateDialogElementBase
implements UpdateProgressObserverInterface, DeviceRequestObserverInterface {
static get is() {
return 'firmware-update-dialog' as const;
}
static get template() {
return getTemplate();
}
static get properties() {
return {
update: {
type: Object,
},
installationProgress: {
type: Object,
value: initialInstallationProgress,
observer:
FirmwareUpdateDialogElement.prototype.installationProgressChanged,
},
isInitiallyInflight: {
value: false,
},
dialogContent: {
type: Object,
value: initialDialogContent,
computed: 'computeDialogContent(installationProgress.*,' +
'isInitiallyInflight, lastDeviceRequestId)',
},
updateIsDone: {
type: Boolean,
value: false,
computed: 'isUpdateDone(installationProgress.state)',
reflectToAttribute: true,
},
* This property is used to keep track of the ID of the last-received
* DeviceRequest. If this property is not null, it means there is a
* pending request. If the property is null, it means there are no pending
* requests.
*/
lastDeviceRequestId: {
type: Object,
value: null,
},
};
}
update: FirmwareUpdate|null = null;
installationProgress: InstallationProgress;
private isInitiallyInflight = false;
private lastDeviceRequestId: DeviceRequestId|null = null;
private updateIsDone: boolean;
dialogContent = initialDialogContent;
private updateProvider = getUpdateProvider();
private installController: InstallControllerRemote|null = null;
private updateProgressObserverReceiver: UpdateProgressObserverReceiver|null =
null;
private deviceRequestObserverReceiver: DeviceRequestObserverReceiver|null =
null;
private systemUtils = getSystemUtils();
private inactiveDialogStates: UpdateState[] =
[UpdateState.kUnknown, UpdateState.kIdle];
override connectedCallback() {
super.connectedCallback();
if (!isAppV2Enabled()) {
this.inactiveDialogStates.push(UpdateState.kWaitingForUser);
}
window.addEventListener(
'open-update-dialog',
(e) => this.onOpenUpdateDialog(
e as CustomEvent<OpenUpdateDialogEventDetail>));
}
onDeviceRequest(request: DeviceRequest): void {
assert(isAppV2Enabled());
if (request.kind !== DeviceRequestKind.kImmediate) {
return;
}
this.lastDeviceRequestId = request.id;
}
onStatusChanged(update: InstallationProgress): void {
if (isAppV2Enabled() && update.state !== UpdateState.kWaitingForUser &&
this.installationProgress.state === UpdateState.kWaitingForUser) {
this.lastDeviceRequestId = null;
}
if (update.state === UpdateState.kSuccess ||
update.state === UpdateState.kFailed) {
this.isInitiallyInflight = false;
}
this.installationProgress = update;
}
protected installationProgressChanged(
prevProgress: FirmwareUpdateDialogElement['installationProgress'],
currProgress: FirmwareUpdateDialogElement['installationProgress']): void {
if (!currProgress || prevProgress.state == currProgress.state) {
return;
}
assert(this.shadowRoot);
const dialogTitle =
this.shadowRoot.querySelector<HTMLElement>('#updateDialogTitle');
if (dialogTitle) {
dialogTitle.focus();
}
}
protected closeDialog(): void {
this.isInitiallyInflight = false;
this.installationProgress = initialInstallationProgress;
this.update = null;
}
protected async prepareForUpdate(): Promise<void> {
assert(this.update);
const response =
await this.updateProvider.prepareForUpdate(this.update.deviceId);
if (!response.controller) {
return;
}
this.installController = response.controller;
this.bindReceiverAndMaybeStartUpdate();
}
protected bindReceiverAndMaybeStartUpdate(): void {
this.updateProgressObserverReceiver =
new UpdateProgressObserverReceiver(this);
assert(this.installController);
this.installController.addUpdateProgressObserver(
this.updateProgressObserverReceiver.$.bindNewPipeAndPassRemote());
if (isAppV2Enabled()) {
this.deviceRequestObserverReceiver =
new DeviceRequestObserverReceiver(this);
this.installController.addDeviceRequestObserver(
this.deviceRequestObserverReceiver.$.bindNewPipeAndPassRemote());
}
if (!this.isInitiallyInflight) {
assert(this.update);
this.installController.beginUpdate(
this.update.deviceId, this.update.filepath);
}
}
protected shouldShowUpdateDialog(): boolean {
if (!this.update) {
return false;
}
if (this.isInitiallyInflight) {
return true;
}
const activeDialogStates: UpdateState[] = [
UpdateState.kUpdating,
UpdateState.kRestarting,
UpdateState.kFailed,
UpdateState.kSuccess,
];
if (isAppV2Enabled()) {
activeDialogStates.push(UpdateState.kWaitingForUser);
}
return activeDialogStates.includes(this.installationProgress.state) ||
this.installationProgress.percentage > 0;
}
protected computePercentageValue(): number {
if (this.installationProgress?.percentage) {
return this.installationProgress.percentage;
}
return 0;
}
protected isUpdateInProgress(): boolean {
if (this.inactiveDialogStates.includes(this.installationProgress.state)) {
return this.installationProgress.percentage > 0;
}
return this.installationProgress.state === UpdateState.kUpdating;
}
protected isDeviceRestarting(): boolean {
return this.installationProgress.state === UpdateState.kRestarting;
}
protected shouldShowProgressBar(): boolean {
const showProgressBar = this.isUpdateInProgress() ||
this.isDeviceRestarting() || this.isWaitingForUserAction() ||
this.isInitiallyInflight;
assert(this.shadowRoot);
const progressIsActiveEl = this.shadowRoot.activeElement ==
this.shadowRoot.querySelector('#progress');
const dialogTitle =
this.shadowRoot.querySelector<HTMLElement>('#updateDialogTitle');
if (progressIsActiveEl && !showProgressBar && dialogTitle) {
dialogTitle.focus();
}
return showProgressBar;
}
protected isUpdateDone(): boolean {
return this.installationProgress.state === UpdateState.kSuccess ||
this.installationProgress.state === UpdateState.kFailed;
}
createRequestDialogContent(): DialogContent {
assert(this.update);
const {deviceName} = this.update;
const {percentage} = this.installationProgress;
assert(this.lastDeviceRequestId !== null);
const deviceNameString: string = deviceName;
return {
title: this.i18n('updating', deviceNameString),
body: this.getI18nStringForDeviceRequestId(
this.lastDeviceRequestId, deviceNameString),
footer: this.i18n('waitingFooterText', percentage),
};
}
createDialogContentObj(state: UpdateState): DialogContent {
assert(this.update);
const {deviceName, deviceVersion, needsReboot} = this.update;
const {percentage} = this.installationProgress;
const dialogContent = new Map<UpdateState, DialogContent>([
[
UpdateState.kUpdating,
{
title: this.i18n('updating', deviceName),
body: this.i18n('updatingInfo'),
footer: this.i18n('installing', percentage),
},
],
[
UpdateState.kRestarting,
{
title: this.i18n('restartingTitleText', deviceName),
body: this.i18n('restartingBodyText'),
footer: this.i18n('restartingFooterText'),
},
],
[
UpdateState.kFailed,
{
title: this.i18n('updateFailedTitleText', deviceName),
body: this.i18n('updateFailedBodyText'),
footer: '',
},
],
]);
if (needsReboot) {
dialogContent.set(UpdateState.kSuccess, {
title: this.i18n('deviceReadyToInstallUpdate', deviceName),
body: this.i18n('deviceNeedsReboot', deviceName, deviceVersion),
footer: '',
});
} else {
dialogContent.set(UpdateState.kSuccess, {
title: this.i18n('deviceUpToDate', deviceName),
body: this.i18n('hasBeenUpdated', deviceName, deviceVersion),
footer: '',
});
}
assert(dialogContent.has(state));
return dialogContent.get(state) as DialogContent;
}
computeDialogContent(): DialogContent {
if (!this.isInitiallyInflight && !this.update) {
return initialDialogContent;
}
if (this.inactiveDialogStates.includes(this.installationProgress.state) ||
this.isDeviceRestarting()) {
return this.createDialogContentObj(UpdateState.kRestarting);
}
if (this.isInitiallyInflight || this.isUpdateInProgress()) {
return this.createDialogContentObj(UpdateState.kUpdating);
}
if (isAppV2Enabled() &&
this.installationProgress.state === UpdateState.kWaitingForUser) {
if (this.lastDeviceRequestId === null) {
return this.createDialogContentObj(UpdateState.kUpdating);
} else {
return this.createRequestDialogContent();
}
}
if (this.isUpdateDone()) {
return this.createDialogContentObj(this.installationProgress.state);
}
return initialDialogContent;
}
protected isInIndeterminateState(): boolean {
if (this.installationProgress) {
return this.inactiveDialogStates.includes(
this.installationProgress.state) ||
this.isDeviceRestarting();
}
return false;
}
protected isProgressBarDisabled(): boolean {
return this.isWaitingForUserAction();
}
protected isUpdateSuccessfulAndRequiresReboot(): boolean {
assert(this.update);
return this.installationProgress.state === UpdateState.kSuccess &&
this.update.needsReboot;
}
protected computeButtonText(): string {
if (!this.isUpdateDone()) {
return '';
}
return this.installationProgress.state === UpdateState.kSuccess ?
this.i18n('doneButton') :
this.i18n('okButton');
}
protected restartDevice(): void {
assert(this.isUpdateDone());
this.systemUtils.restart();
return;
}
protected isDialogOpen(): boolean {
assert(this.shadowRoot);
return !!this.shadowRoot.querySelector('#updateDialog');
}
private onOpenUpdateDialog(e: CustomEvent<OpenUpdateDialogEventDetail>):
void {
this.update = e.detail.update;
this.isInitiallyInflight = e.detail.inflight;
this.prepareForUpdate();
}
setIsInitiallyInflightForTesting(isInitiallyInflight: boolean): void {
this.isInitiallyInflight = isInitiallyInflight;
}
private isWaitingForUserAction(): boolean {
return isAppV2Enabled() && this.lastDeviceRequestId !== null &&
this.installationProgress.state === UpdateState.kWaitingForUser;
}
private getDialogBodyAriaLive(): string {
return this.isWaitingForUserAction() ? 'assertive' : '';
}
private getStringIdForDeviceRequestId(deviceRequestId: DeviceRequestId):
string {
switch (deviceRequestId) {
case (DeviceRequestId.kDoNotPowerOff):
return 'requestIdDoNotPowerOff';
case (DeviceRequestId.kReplugInstall):
return 'requestIdReplugInstall';
case (DeviceRequestId.kInsertUSBCable):
return 'requestIdInsertUsbCable';
case (DeviceRequestId.kRemoveUSBCable):
return 'requestIdRemoveUsbCable';
case (DeviceRequestId.kPressUnlock):
return 'requestIdPressUnlock';
case (DeviceRequestId.kRemoveReplug):
return 'requestIdRemoveReplug';
case (DeviceRequestId.kReplugPower):
return 'requestIdReplugPower';
}
}
private getI18nStringForDeviceRequestId(
deviceRequestId: DeviceRequestId, deviceName: string): string {
const requestStringId = this.getStringIdForDeviceRequestId(deviceRequestId);
if (deviceRequestId == DeviceRequestId.kDoNotPowerOff) {
return this.i18n(requestStringId);
}
return this.i18n(requestStringId, deviceName);
}
}
declare global {
interface HTMLElementEventMap {
'open-update-dialog': CustomEvent<OpenUpdateDialogEventDetail>;
}
interface HTMLElementTagNameMap {
[FirmwareUpdateDialogElement.is]: FirmwareUpdateDialogElement;
}
}
customElements.define(
FirmwareUpdateDialogElement.is, FirmwareUpdateDialogElement);