chromium/chrome/browser/resources/ash/settings/nearby_share_page/nearby_share_high_visibility_page.ts

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

/**
 * @fileoverview The 'nearby-high-visibility-page' component is opened when the
 * user is broadcast in high-visibility mode. The user may cancel to stop high
 * visibility mode at any time.
 */

import 'chrome://resources/ash/common/cr_elements/cr_lottie/cr_lottie.js';
import 'chrome://resources/cros_components/lottie_renderer/lottie-renderer.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import 'chrome://resources/ash/common/cr_elements/localized_link/localized_link.js';
import '/shared/nearby_page_template.js';
import '/shared/nearby_shared_icons.html.js';

import {RegisterReceiveSurfaceResult} from '/shared/nearby_share.mojom-webui.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {getTemplate} from './nearby_share_high_visibility_page.html.js';

/**
 * Represents the current error state, if one exists.
 */
enum NearbyVisibilityErrorState {
  TIMED_OUT = 0,
  NO_CONNECTION_MEDIUM = 1,
  TRANSFER_IN_PROGRESS = 2,
  SOMETHING_WRONG = 3,
}

/**
 * The pulse animation asset URL.
 */
const PULSE_ANIMATION_URL = 'nearby_share_pulse_animation.json';

const NearbyShareHighVisibilityPageElementBase = I18nMixin(PolymerElement);

export class NearbyShareHighVisibilityPageElement extends
    NearbyShareHighVisibilityPageElementBase {
  static get is() {
    return 'nearby-share-high-visibility-page' as const;
  }

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

  static get properties() {
    return {
      deviceName: {
        notify: true,
        type: String,
        value: 'DEVICE_NAME_NOT_SET',
      },

      /**
       * DOMHighResTimeStamp in milliseconds of when high visibility will be
       * turned off.
       */
      shutoffTimestamp: {
        type: Number,
        value: 0,
      },

      /**
       * Calculated value of remaining seconds of high visibility mode.
       * Initialized to -1 to differentiate it from timed out state.
       */
      remainingTimeInSeconds_: {
        type: Number,
        value: -1,
      },

      registerResult: {
        type: RegisterReceiveSurfaceResult,
        value: null,
      },

      nearbyProcessStopped: {
        type: Boolean,
        value: false,
      },

      startAdvertisingFailed: {
        type: Boolean,
        value: false,
      },

      /**
       * A null |setupState_| indicates that the operation has not yet started.
       */
      errorState_: {
        type: Number,
        value: null,
        computed:
            'computeErrorState_(shutoffTimestamp, remainingTimeInSeconds_,' +
            'registerResult, nearbyProcessStopped, startAdvertisingFailed)',
      },
    };
  }

  deviceName: string;
  nearbyProcessStopped: boolean;
  registerResult: RegisterReceiveSurfaceResult|null;
  shutoffTimestamp: number;
  startAdvertisingFailed: boolean;
  private errorState_: NearbyVisibilityErrorState|null;
  private remainingTimeInSeconds_: number;
  private remainingTimeIntervalId_: number;

  constructor() {
    super();

    this.remainingTimeIntervalId_ = -1;
  }

  override connectedCallback(): void {
    super.connectedCallback();

    this.calculateRemainingTime_();
    this.remainingTimeIntervalId_ = setInterval(() => {
      this.calculateRemainingTime_();
    }, 1000);
  }

  override disconnectedCallback(): void {
    super.disconnectedCallback();

    if (this.remainingTimeIntervalId_ !== -1) {
      clearInterval(this.remainingTimeIntervalId_);
      this.remainingTimeIntervalId_ = -1;
    }
  }

  private highVisibilityTimedOut_(): boolean {
    // High visibility session is timed out only if remaining seconds is 0 AND
    // timestamp is also set.
    return (this.remainingTimeInSeconds_ === 0) &&
        (this.shutoffTimestamp !== 0);
  }

  private computeErrorState_(): NearbyVisibilityErrorState|null {
    if (this.registerResult ===
        RegisterReceiveSurfaceResult.kNoConnectionMedium) {
      return NearbyVisibilityErrorState.NO_CONNECTION_MEDIUM;
    }
    if (this.registerResult ===
        RegisterReceiveSurfaceResult.kTransferInProgress) {
      return NearbyVisibilityErrorState.TRANSFER_IN_PROGRESS;
    }
    if (this.highVisibilityTimedOut_()) {
      return NearbyVisibilityErrorState.TIMED_OUT;
    }
    if (this.registerResult === RegisterReceiveSurfaceResult.kFailure ||
        this.nearbyProcessStopped || this.startAdvertisingFailed) {
      return NearbyVisibilityErrorState.SOMETHING_WRONG;
    }
    return null;
  }

  private getErrorTitle_(): string {
    switch (this.errorState_) {
      case NearbyVisibilityErrorState.TIMED_OUT:
        return this.i18n('nearbyShareErrorTimeOut');
      case NearbyVisibilityErrorState.NO_CONNECTION_MEDIUM:
        return this.i18n('nearbyShareErrorNoConnectionMedium');
      case NearbyVisibilityErrorState.TRANSFER_IN_PROGRESS:
        return this.i18n('nearbyShareErrorTransferInProgressTitle');
      case NearbyVisibilityErrorState.SOMETHING_WRONG:
        return this.i18n('nearbyShareErrorCantReceive');
      default:
        return '';
    }
  }

  private getErrorDescription_(): TrustedHTML|string {
    switch (this.errorState_) {
      case NearbyVisibilityErrorState.TIMED_OUT:
        return this.i18nAdvanced('nearbyShareHighVisibilityTimeoutText');
      case NearbyVisibilityErrorState.NO_CONNECTION_MEDIUM:
        return this.i18n('nearbyShareErrorNoConnectionMediumDescription');
      case NearbyVisibilityErrorState.TRANSFER_IN_PROGRESS:
        return this.i18n('nearbyShareErrorTransferInProgressDescription');
      case NearbyVisibilityErrorState.SOMETHING_WRONG:
        return this.i18n('nearbyShareErrorSomethingWrong');
      default:
        return '';
    }
  }

  private getSubTitle_(): string {
    if (this.remainingTimeInSeconds_ === -1) {
      return '';
    }

    let timeValue = '';
    if (this.remainingTimeInSeconds_ > 60) {
      timeValue = this.i18n(
          'nearbyShareHighVisibilitySubTitleMinutes',
          Math.ceil(this.remainingTimeInSeconds_ / 60));
    } else {
      timeValue = this.i18n(
          'nearbyShareHighVisibilitySubTitleSeconds',
          this.remainingTimeInSeconds_);
    }

    return this.i18n(
        'nearbyShareHighVisibilitySubTitle', this.deviceName, timeValue);
  }

  private calculateRemainingTime_(): void {
    if (this.shutoffTimestamp === 0) {
      return;
    }

    const now = performance.now();
    const remainingTimeInMs =
        this.shutoffTimestamp > now ? this.shutoffTimestamp - now : 0;
    this.remainingTimeInSeconds_ = Math.ceil(remainingTimeInMs / 1000);
  }

  /**
   * Announce the remaining time for screen readers. Only announce once per
   * minute to avoid overwhelming user. Though this gets called once every
   * second, the value returned only changes each minute.
   * @return The alternate page subtitle to be used as an aria-live
   *     announcement for screen readers.
   */
  private getA11yAnnouncedSubTitle_(): string {
    // Skip announcement for 0 seconds left to avoid alerting on time out.
    // There is a separate time out alert shown in the error section.
    if (this.remainingTimeInSeconds_ === 0) {
      return '';
    }
    const remainingMinutes = this.remainingTimeInSeconds_ > 0 ?
        Math.ceil(this.remainingTimeInSeconds_ / 60) :
        5;

    const timeValue =
        this.i18n('nearbyShareHighVisibilitySubTitleMinutes', remainingMinutes);

    return this.i18n(
        'nearbyShareHighVisibilitySubTitle', this.deviceName, timeValue);
  }

  /**
   * Returns the URL for the asset that defines the high visibility page's
   * pulsing background animation.
   */
  private getAnimationUrl_(): string {
    return PULSE_ANIMATION_URL;
  }
}

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

customElements.define(
    NearbyShareHighVisibilityPageElement.is,
    NearbyShareHighVisibilityPageElement);