chromium/ash/webui/diagnostics_ui/resources/battery_status_card.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.

import 'chrome://resources/ash/common/cr_elements/icons.html.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import './data_point.js';
import './diagnostics_card.js';
import './diagnostics_shared.css.js';
import './icons.html.js';
import './percent_bar_chart.js';
import './routine_section.js';
import './strings.m.js';

import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {assertNotReached} from 'chrome://resources/js/assert.js';
import {mojoString16ToString} from 'chrome://resources/js/mojo_type_util.js';
import {PolymerElementProperties} from 'chrome://resources/polymer/v3_0/polymer/interfaces.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {getTemplate} from './battery_status_card.html.js';
import {getDiagnosticsIcon} from './diagnostics_utils.js';
import {getSystemDataProvider} from './mojo_interface_provider.js';
import {TestSuiteStatus} from './routine_list_executor.js';
import {BatteryChargeStatus, BatteryChargeStatusObserverReceiver, BatteryHealth, BatteryHealthObserverReceiver, BatteryInfo, BatteryState, ExternalPowerSource, SystemDataProviderInterface} from './system_data_provider.mojom-webui.js';
import {RoutineType} from './system_routine_controller.mojom-webui.js';

const BATTERY_ICON_PREFIX = 'battery-';

/**
 * Calculates the battery percentage in the range percentage [0,100].
 * @param chargeNow Current battery charge in milliamp hours.
 * @param chargeFull Full battery charge in milliamp hours.
 */
function calculatePowerPercentage(
    chargeNow: number, chargeFull: number): number {
  // Handle values in battery_info which could cause a SIGFPE. See b/227485637.
  if (chargeFull == 0 || isNaN(chargeNow) || isNaN(chargeFull)) {
    return 0;
  }

  const percent = Math.round(100 * chargeNow / chargeFull);
  return Math.min(Math.max(percent, 0), 100);
}

/**
 * @fileoverview
 * 'battery-status-card' shows information about battery status.
 */

const BatteryStatusCardElementBase = I18nMixin(PolymerElement);

export class BatteryStatusCardElement extends BatteryStatusCardElementBase {
  static get is(): string {
    return 'battery-status-card';
  }

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

  static get properties(): PolymerElementProperties {
    return {
      batteryChargeStatus: {
        type: Object,
      },

      batteryHealth: {
        type: Object,
      },

      batteryInfo: {
        type: Object,
      },

      routines: {
        type: Array,
        computed:
            'getCurrentPowerRoutines(batteryChargeStatus.powerAdapterStatus)',
      },

      powerTimeString: {
        type: String,
        computed: 'getPowerTimeString(batteryChargeStatus.powerTime)',
      },

      testSuiteStatus: {
        type: Number,
        value: TestSuiteStatus.NOT_RUNNING,
        notify: true,
      },

      batteryIcon: {
        type: String,
        computed: 'getBatteryIcon(batteryChargeStatus.powerAdapterStatus,' +
            'batteryChargeStatus.chargeNowMilliampHours,' +
            'batteryHealth.chargeFullNowMilliampHours)',
      },

      iconClass: {
        type: String,
        computed: 'updateIconClassList(batteryChargeStatus.powerAdapterStatus)',
      },

      isActive: {
        type: Boolean,
      },

    };
  }


  testSuiteStatus: TestSuiteStatus;
  batteryIcon: string;
  iconClass: string;
  isActive: boolean;
  private batteryChargeStatus: BatteryChargeStatus;
  private batteryHealth: BatteryHealth;
  private batteryInfo: BatteryInfo;
  private routines: RoutineType[];
  private powerTimeString: string;
  private systemDataProvider: SystemDataProviderInterface =
      getSystemDataProvider();
  private batteryChargeStatusObserverReceiver:
      BatteryChargeStatusObserverReceiver|null;
  private batteryHealthObserverReceiver: BatteryHealthObserverReceiver|null;

  constructor() {
    super();
    this.fetchBatteryInfo();
    this.observeBatteryChargeStatus();
    this.observeBatteryHealth();
  }

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

    if (this.batteryChargeStatusObserverReceiver) {
      this.batteryChargeStatusObserverReceiver.$.close();
    }
    if (this.batteryHealthObserverReceiver) {
      this.batteryHealthObserverReceiver.$.close();
    }
  }

  getBatteryChargeStatusForTesting(): BatteryChargeStatus {
    return this.batteryChargeStatus;
  }

  private fetchBatteryInfo(): void {
    this.systemDataProvider.getBatteryInfo().then(
        (result: {batteryInfo: BatteryInfo}) => {
          this.onBatteryInfoReceived(result.batteryInfo);
        });
  }

  private onBatteryInfoReceived(batteryInfo: BatteryInfo): void {
    this.batteryInfo = batteryInfo;
  }

  private observeBatteryChargeStatus(): void {
    this.batteryChargeStatusObserverReceiver =
        new BatteryChargeStatusObserverReceiver(this);

    this.systemDataProvider.observeBatteryChargeStatus(
        this.batteryChargeStatusObserverReceiver.$.bindNewPipeAndPassRemote());
  }

  /**
   * Implements BatteryChargeStatusObserver.onBatteryChargeStatusUpdated()
   */
  onBatteryChargeStatusUpdated(batteryChargeStatus: BatteryChargeStatus): void {
    this.batteryChargeStatus = batteryChargeStatus;
  }

  private observeBatteryHealth(): void {
    this.batteryHealthObserverReceiver =
        new BatteryHealthObserverReceiver(this);

    this.systemDataProvider.observeBatteryHealth(
        this.batteryHealthObserverReceiver.$.bindNewPipeAndPassRemote());
  }

  /**
   * Get an array of currently relevant routines based on power adaptor status
   */
  private getCurrentPowerRoutines(powerAdapterStatus: ExternalPowerSource):
      RoutineType[] {
    return powerAdapterStatus === ExternalPowerSource.kDisconnected ?
        [RoutineType.kBatteryDischarge] :
        [RoutineType.kBatteryCharge];
  }

  /**
   * Get power time string from battery status.
   */
  protected getPowerTimeString(): string {
    const fullyCharged =
        this.batteryChargeStatus.batteryState === BatteryState.kFull;
    if (fullyCharged) {
      return loadTimeData.getString('batteryFullText');
    }

    const powerTimeStr = this.batteryChargeStatus.powerTime;
    if (!powerTimeStr || powerTimeStr.data.length === 0) {
      return loadTimeData.getString('batteryCalculatingText');
    }

    const timeValue = mojoString16ToString(powerTimeStr);
    const charging =
        this.batteryChargeStatus.powerAdapterStatus === ExternalPowerSource.kAc;

    return charging ?
        loadTimeData.getStringF('batteryChargingStatusText', timeValue) :
        loadTimeData.getStringF('batteryDischargingStatusText', timeValue);
  }

  /**
   * Implements BatteryHealthObserver.onBatteryHealthUpdated()
   */
  onBatteryHealthUpdated(batteryHealth: BatteryHealth): void {
    this.batteryHealth = batteryHealth;
  }

  protected getDesignedFullCharge(): string {
    return loadTimeData.getStringF(
        'batteryChipText', this.batteryHealth.chargeFullDesignMilliampHours);
  }

  protected getBatteryHealth(): string {
    const MAX_PERCENTAGE = 100;
    const batteryWearPercentage =
        Math.min(this.batteryHealth.batteryWearPercentage, MAX_PERCENTAGE);
    return loadTimeData.getStringF('batteryHealthText', batteryWearPercentage);
  }

  protected getCurrentNow(): string {
    return loadTimeData.getStringF(
        'currentNowText', this.batteryChargeStatus.currentNowMilliamps);
  }

  protected getRunTestsButtonText(): string {
    return loadTimeData.getString(
        this.batteryChargeStatus.powerAdapterStatus ===
                ExternalPowerSource.kDisconnected ?
            'runBatteryDischargeTestText' :
            'runBatteryChargeTestText');
  }

  protected getRunTestsAdditionalMessage(): string {
    const batteryInfoMissing = !this.batteryChargeStatus || !this.batteryHealth;
    const notCharging = this.batteryChargeStatus.powerAdapterStatus ===
        ExternalPowerSource.kDisconnected;
    if (notCharging || batteryInfoMissing) {
      return '';
    }

    const disableRunButtonThreshold = 95;
    const percentage = calculatePowerPercentage(
        this.batteryChargeStatus.chargeNowMilliampHours,
        this.batteryHealth.chargeFullNowMilliampHours);

    return percentage >= disableRunButtonThreshold ?
        loadTimeData.getString('batteryChargeTestFullMessage') :
        '';
  }

  protected getEstimateRuntimeInMinutes(): number {
    // Power routines will always last <= 1 minute.
    return 1;
  }

  /**
   * Use the current battery percentage to determine which icon to show the
   * user. Each icon covers a range of 6 or 7 percentage values.
   */
  private getBatteryIconForChargePercentage(): string {
    if (!this.batteryChargeStatus || !this.batteryHealth) {
      return this.batteryIcon;
    }

    const percentage = calculatePowerPercentage(
        this.batteryChargeStatus.chargeNowMilliampHours,
        this.batteryHealth.chargeFullNowMilliampHours);

    // Handle values in battery_info which could cause a SIGFPE. See
    // b/227485637.
    if (percentage === 0) {
      return this.batteryIcon ||
          getDiagnosticsIcon(`${BATTERY_ICON_PREFIX}outline`);
    }

    const iconSizes = [
      [1, 7],
      [8, 14],
      [15, 21],
      [22, 28],
      [29, 35],
      [36, 42],
      [43, 49],
      [50, 56],
      [57, 63],
      [64, 70],
      [71, 77],
      [78, 85],
      [86, 92],
      [93, 100],
    ];

    for (const [rangeStart, rangeEnd] of iconSizes) {
      if (percentage >= rangeStart && percentage <= rangeEnd) {
        return getDiagnosticsIcon(
            `${BATTERY_ICON_PREFIX}${rangeStart}-${rangeEnd}`);
      }
    }

    assertNotReached();
  }

  protected getBatteryIcon(): string {
    const charging = this.batteryChargeStatus &&
        this.batteryChargeStatus.powerAdapterStatus === ExternalPowerSource.kAc;

    if (charging) {
      return getDiagnosticsIcon(`${BATTERY_ICON_PREFIX}charging`);
    }

    return this.getBatteryIconForChargePercentage();
  }

  /**
   * Use the power adapter status to determine if we need to overwrite the value
   * for --iron-icon-stroke-color since the charging icon needs to remove it in
   * order to display properly.
   */
  protected updateIconClassList(): string {
    return (this.batteryChargeStatus &&
            this.batteryChargeStatus.powerAdapterStatus ===
                ExternalPowerSource.kAc) ?
        'remove-stroke' :
        '';
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'battery-status-card': BatteryStatusCardElement;
  }
}

customElements.define(BatteryStatusCardElement.is, BatteryStatusCardElement);