chromium/ash/webui/shimless_rma/resources/onboarding_update_page.ts

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'chrome://resources/ash/common/cr_elements/icons.html.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import 'chrome://resources/polymer/v3_0/paper-spinner/paper-spinner-lite.js';
import 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import './shimless_rma_shared.css.js';
import './base_page.js';
import './icons.html.js';

import {assert} from 'chrome://resources/js/assert.js';
import {CrDialogElement} from 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {getShimlessRmaService} from './mojo_interface_provider.js';
import {getTemplate} from './onboarding_update_page.html.js';
import {HardwareVerificationStatusObserverReceiver, OsUpdateObserverReceiver, OsUpdateOperation, ShimlessRmaServiceInterface, StateResult, UpdateErrorCode} from './shimless_rma.mojom-webui.js';
import {disableAllButtons, enableAllButtons, enableNextButton, focusPageTitle} from './shimless_rma_util.js';

/**
 * @fileoverview
 * 'onboarding-update-page' is the page shown when there is an Chrome OS update
 * available on the device for the user to install before the RMA process.
 */

const OnboardingUpdatePageElementBase = I18nMixin(PolymerElement);

export class OnboardingUpdatePageElement extends
    OnboardingUpdatePageElementBase {
  static get is() {
    return 'onboarding-update-page' as const;
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {
      /**
       * Set by shimless_rma.ts.
       */
      allButtonsDisabled: Boolean,

      currentVersionText: {
        type: String,
        value: '',
      },

      updateVersionButtonLabel: {
        type: String,
        value: '',
      },

      updateInProgress: {
        type: Boolean,
        value: false,
        observer:
            OnboardingUpdatePageElement.prototype.onUpdateInProgressChange,
      },

      verificationFailedMessage: {
        type: String,
        value: '',
      },

      /**
       * A string containing a list of the unqualified component identifiers
       * separated by new lines.
       */
      unqualifiedComponentsText: {
        type: String,
        value: '',
      },


      osUpdateEncounteredError: {
        type: Boolean,
        value: false,
      },
    };
  }

  allButtonsDisabled: boolean;
  shimlessRmaService: ShimlessRmaServiceInterface;
  isCompliant: boolean;
  protected currentVersionText: string;
  protected updateVersionButtonLabel: string;
  protected updateInProgress: boolean;
  protected verificationFailedMessage: TrustedHTML;
  protected unqualifiedComponentsText: string;
  protected osUpdateEncounteredError: boolean;
  protected currentVersion: string;
  protected osUpdateObserverReceiver: OsUpdateObserverReceiver|null;
  protected hwVerificationObserverReceiver: HardwareVerificationStatusObserverReceiver|null;

  constructor() {
    super();
    if (!loadTimeData.getBoolean('osUpdateEnabled')) {
      return;
    }

    this.shimlessRmaService = getShimlessRmaService();
    this.currentVersion = '';
    this.osUpdateObserverReceiver = new OsUpdateObserverReceiver(this);

    this.shimlessRmaService.observeOsUpdateProgress(
        this.osUpdateObserverReceiver.$.bindNewPipeAndPassRemote());

    // We assume it's compliant until updated in onHardwareVerificationResult().
    this.isCompliant = true;
    this.hwVerificationObserverReceiver = new HardwareVerificationStatusObserverReceiver(this);

    this.shimlessRmaService.observeHardwareVerificationStatus(
        this.hwVerificationObserverReceiver.$.bindNewPipeAndPassRemote());
  }

  override ready() {
    super.ready();
    if (!loadTimeData.getBoolean('osUpdateEnabled')) {
      return;
    }
    this.getCurrentVersionText();
    this.getUpdateVersionNumber();
    enableNextButton(this);

    focusPageTitle(this);
  }

  private getCurrentVersionText(): void {
    if (!loadTimeData.getBoolean('osUpdateEnabled')) {
      return;
    }
    this.shimlessRmaService.getCurrentOsVersion().then((res: {version: string|null}) => {
      if (res.version != null) {
        this.currentVersion = res.version;
      } else {
        this.currentVersion = '0.0.0.0';
      }
      this.currentVersionText =
          this.i18n('currentVersionOutOfDateText', this.currentVersion);
    });
  }

  private getUpdateVersionNumber(): void {
    if (!loadTimeData.getBoolean('osUpdateEnabled')) {
      return;
    }
    this.shimlessRmaService.checkForOsUpdates().then((res: {updateAvailable: boolean, version: string|null}) => {
      assert(res.updateAvailable);
      this.updateVersionButtonLabel =
          this.i18n('updateVersionRestartLabel', res?.version || '');
    });
  }

  private updateOs(): void {
    this.updateInProgress = true;
    this.shimlessRmaService.updateOs().then((res: {updateStarted: boolean}) => {
      if (!res.updateStarted) {
        this.updateInProgress = false;
      }
    });
  }

  protected onUpdateButtonClicked(): void {
    if (!loadTimeData.getBoolean('osUpdateEnabled')) {
      return;
    }

    this.updateOs();
  }

  protected onRetryUpdateButtonClicked(): void {
    if (!loadTimeData.getBoolean('osUpdateEnabled')) {
      return;
    }

    assert(this.osUpdateEncounteredError);
    this.osUpdateEncounteredError = false;

    this.updateOs();
  }

  onNextButtonClick(): Promise<{stateResult: StateResult}> {
    return this.shimlessRmaService.updateOsSkipped();
  }

  /**
   * Implements OsUpdateObserver.onOsUpdateProgressUpdated()
   */
  onOsUpdateProgressUpdated(operation: OsUpdateOperation, _progress: number, error: UpdateErrorCode): void {
    if (!loadTimeData.getBoolean('osUpdateEnabled')) {
      return;
    }
    // Ignore progress when not updating, it is just the update available check.
    if (!this.updateInProgress) {
      return;
    }

    if (operation === OsUpdateOperation.kIdle ||
        operation === OsUpdateOperation.kReportingErrorEvent ||
        operation === OsUpdateOperation.kNeedPermissionToUpdate ||
        operation === OsUpdateOperation.kDisabled) {
      this.updateInProgress = false;

      if (error !== UpdateErrorCode.kSuccess) {
        this.osUpdateEncounteredError = true;
      }
    }
  }

  /**
   * Implements
   * HardwareVerificationStatusObserver.onHardwareVerificationResult()
   */
  onHardwareVerificationResult(isCompliant: boolean, errorMessage: string): void {
    if (!loadTimeData.getBoolean('osUpdateEnabled')) {
      return;
    }
    this.isCompliant = isCompliant;

    if (!this.isCompliant) {
      this.unqualifiedComponentsText = errorMessage;
      this.setVerificationFailedMessage();
    }
  }

  private setVerificationFailedMessage(): void {
    if (!loadTimeData.getBoolean('osUpdateEnabled')) {
      return;
    }
    this.verificationFailedMessage = this.i18nAdvanced(
        'osUpdateUnqualifiedComponentsTopText', {attrs: ['id']});

    // The #unqualifiedComponentsLink identifier is sourced from the string
    // attached to `osUpdateUnqualifiedComponentsTopText` in the related .grd
    // file.
    const linkElement: HTMLAnchorElement|null =
        this.shadowRoot!.querySelector('#unqualifiedComponentsLink');
    assert(linkElement);
    linkElement.setAttribute('href', '#');
    const dialog: CrDialogElement|null = this.shadowRoot!.querySelector('#unqualifiedComponentsDialog');
    assert(dialog);
    linkElement.addEventListener('click', () => dialog.showModal());
  }

  private closeDialog(): void {
    if (!loadTimeData.getBoolean('osUpdateEnabled')) {
      return;
    }
    const dialog: CrDialogElement|null = this.shadowRoot!.querySelector('#unqualifiedComponentsDialog');
    assert(dialog);
    dialog.close();
  }

  private onUpdateInProgressChange(): void {
    if (!loadTimeData.getBoolean('osUpdateEnabled')) {
      return;
    }
    if (this.updateInProgress) {
      disableAllButtons(this, /*showBusyStateOverlay=*/ false);
    } else {
      enableAllButtons(this);
    }
  }

  protected shouldShowUpdateInstructions(): boolean {
    return !this.updateInProgress && !this.osUpdateEncounteredError;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    [OnboardingUpdatePageElement.is]: OnboardingUpdatePageElement;
  }
}

customElements.define(
    OnboardingUpdatePageElement.is, OnboardingUpdatePageElement);