chromium/ui/file_manager/file_manager/containers/cloud_panel_container.ts

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

/**
 * @fileoverview Maintains the state of the `<xf-cloud-panel>` and ensures the
 * information is passed to it appropriately.
 */

import {canBulkPinningCloudPanelShow} from '../common/js/util.js';
import type {State} from '../state/state.js';
import {getStore, type Store} from '../state/store.js';
import {type CloudPanelSettingsClickEvent, CloudPanelType, XfCloudPanel} from '../widgets/xf_cloud_panel.js';

export type BulkPinProgress = chrome.fileManagerPrivate.BulkPinProgress;
export const BulkPinStage = chrome.fileManagerPrivate.BulkPinStage;

/**
 * The `CloudPanelContainer` acts as a translation layer between the data in the
 * store relating to bulk pinning and the `<xf-cloud-panel>` element that
 * presents the data.
 */
export class CloudPanelContainer {
  /**
   * The store element.
   */
  private store_: Store;

  /**
   * The previously stored progress, used to identify if any changes have
   * occurred. Store updates could happen to any key within the store, so make
   * sure it's to one that this container uses.
   */
  private progress_: BulkPinProgress|null = null;

  /**
   * The driveFsBulkPinningEnabled preference, used to identify if it has
   * changed or not.
   */
  private bulkPinningEnabled_: boolean = false;

  /**
   * If true, drive syncing is paused due to both being on a network reporting
   * as metered and also having the preference of syncing disabled on metered
   * networks.
   */
  private isOnMetered_: boolean = false;

  /**
   * Keeps track of the number of updates.
   * NOTE: Used for testing only.
   */
  private updates_ = 0;

  constructor(private panel_: XfCloudPanel, private test_: boolean = false) {
    this.store_ = getStore();
    this.store_.subscribe(this);

    this.panel_.addEventListener(
        XfCloudPanel.events.DRIVE_SETTINGS_CLICKED,
        this.onDriveSettingsClick_.bind(this));
  }

  get updates() {
    return this.updates_;
  }

  /**
   * When the "Google Drive settings" button is clicked, open OS Settings to the
   * Google Drive page.
   */
  private onDriveSettingsClick_(_: CloudPanelSettingsClickEvent) {
    chrome.fileManagerPrivate.openSettingsSubpage('googleDrive');
  }

  onStateChanged(state: State) {
    const progress = state.bulkPinning;
    const enabled = !!state.preferences?.driveFsBulkPinningEnabled;
    if (!progress) {
      this.bulkPinningEnabled_ = enabled;
      return;
    }

    const isOnMetered = state.drive?.connectionType ===
        chrome.fileManagerPrivate.DriveConnectionStateType.METERED;

    // Check if any of the required state has changed between store changes.
    if ((this.progress_ &&
         (this.progress_.stage === progress.stage &&
          this.progress_.filesToPin === progress.filesToPin &&
          this.progress_.pinnedBytes === progress.pinnedBytes &&
          this.progress_.bytesToPin === progress.bytesToPin &&
          this.progress_.remainingSeconds === progress.remainingSeconds)) &&
        this.bulkPinningEnabled_ === enabled &&
        this.isOnMetered_ === isOnMetered) {
      return;
    }

    this.progress_ = progress;
    this.bulkPinningEnabled_ = enabled;
    this.isOnMetered_ = isOnMetered;

    // If the bulk pinning cloud panel can't be shown, make sure to close any
    // open variants of it. This ensures if the panel is open when the
    // preference is disabled, it will not stay open with stale data.
    if (!canBulkPinningCloudPanelShow(progress.stage, enabled)) {
      this.panel_.close();
      return;
    }

    // When on a metered network, drive syncing is paused.
    if (this.isOnMetered_) {
      this.updatePanelType_(CloudPanelType.METERED_NETWORK);
      return;
    }

    // If the bulk pinning is paused, this indicates that it is currently
    // offline or battery saver mode is active.
    if (progress.stage === BulkPinStage.PAUSED_OFFLINE) {
      this.updatePanelType_(CloudPanelType.OFFLINE);
      return;
    }
    if (progress.stage === BulkPinStage.PAUSED_BATTERY_SAVER) {
      this.updatePanelType_(CloudPanelType.BATTERY_SAVER);
      return;
    }

    // Not enough space indicates the available local storage is insufficient to
    // store all the files required by the users My drive.
    if (progress.stage === BulkPinStage.NOT_ENOUGH_SPACE) {
      this.updatePanelType_(CloudPanelType.NOT_ENOUGH_SPACE);
      return;
    }
    this.panel_.removeAttribute('type');

    // Files to pin can't be negative the pinned bytes should never exceed
    // the bytes to pin (>100%).
    if (progress.filesToPin < 0 ||
        (progress.pinnedBytes > progress.bytesToPin)) {
      return;
    }

    this.clearAllAttributes_();
    this.panel_.setAttribute('items', String(progress.filesToPin));
    const percentage = (progress.bytesToPin === 0) ?
        '100' :
        (progress.pinnedBytes / progress.bytesToPin * 100).toFixed(0);
    if ((progress.filesToPin > 0 && progress.pinnedBytes > 0) ||
        (progress.pinnedBytes === 0 && progress.bytesToPin === 0) ||
        (progress.filesToPin === 0)) {
      this.panel_.setAttribute('percentage', String(percentage));
    }
    this.panel_.setAttribute('seconds', String(progress.remainingSeconds));
    this.increaseUpdates_();
  }

  /**
   * Updates the underlying panel to the `type` and removes the in progress
   * attributes.
   */
  private updatePanelType_(type: CloudPanelType) {
    this.panel_.setAttribute('type', type);
    this.clearAllAttributes_();
    this.increaseUpdates_();
  }

  /**
   * Clear all the attributes in anticipation of setting new ones.
   */
  private clearAllAttributes_() {
    this.panel_.removeAttribute('items');
    this.panel_.removeAttribute('percentage');
    this.panel_.removeAttribute('seconds');
  }

  /**
   * If in a test environment, keep track of the number of updates that have
   * been performed based on state changes.
   */
  private increaseUpdates_() {
    if (this.test_) {
      ++this.updates_;
    }
  }
}