chromium/ui/file_manager/file_manager/background/js/progress_center.ts

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

import {ProgressCenterItem, ProgressItemState, ProgressItemType} from '../../common/js/progress_center_common.js';
import type {ProgressCenterPanel} from '../../foreground/js/ui/progress_center_panel.js';

/**
 * Implementation of ProgressCenter at the background page.
 */
export class ProgressCenter {
  /**
   * Current items managed by the progress center.
   */
  private items_: ProgressCenterItem[] = [];

  /**
   * List of panel UI managed by the progress center.
   */
  private panels_: ProgressCenterPanel[] = [];

  /**
   * Inhibit end of operation updates for testing.
   */
  private neverNotifyCompleted_ = false;

  /**
   * Turns off sending updates when a file operation reaches 'completed' state.
   * Used for testing UI that can be ephemeral otherwise.
   */
  neverNotifyCompleted() {
    if (window.IN_TEST) {
      this.neverNotifyCompleted_ = true;
    }
  }

  /**
   * Updates the item in the progress center.
   * If the item has a new ID, the item is added to the item list.
   */
  updateItem(item: ProgressCenterItem) {
    // Update item.
    const index = this.getItemIndex_(item.id);
    if (item.state === ProgressItemState.PROGRESSING ||
        item.state === ProgressItemState.SCANNING) {
      if (index === -1) {
        this.items_.push(item);
      } else {
        this.items_[index] = item;
      }
    } else {
      // Error item is not removed until user explicitly dismiss it.
      if (item.state !== ProgressItemState.ERROR && index !== -1) {
        if (this.neverNotifyCompleted_) {
          item.state = ProgressItemState.PROGRESSING;
          return;
        }
        this.items_.splice(index, 1);
      }
    }

    // Update panels.
    for (const panelItem of this.panels_) {
      panelItem.updateItem(item);
    }
  }

  /**
   * Requests to cancel the progress item.
   * @param id Progress ID to be requested to cancel.
   */
  requestCancel(id: string) {
    const item = this.getItemById(id);
    if (item && item.cancelCallback) {
      item.cancelCallback();
    }
  }

  /**
   * Adds a panel UI to the notification center.
   * @param panel Panel UI.
   */
  addPanel(panel: ProgressCenterPanel) {
    if (this.panels_.indexOf(panel) !== -1) {
      return;
    }

    // Update the panel list.
    this.panels_.push(panel);

    // Set the current items.
    for (const item of this.items_) {
      panel.updateItem(item);
    }

    // Register the cancel callback.
    panel.cancelCallback = this.requestCancel.bind(this);

    // Register the dismiss error item callback.
    panel.dismissErrorItemCallback = this.dismissErrorItem_.bind(this);
  }

  /**
   * Removes a panel UI from the notification center.
   * @param panel Panel UI.
   */
  removePanel(panel: ProgressCenterPanel) {
    const index = this.panels_.indexOf(panel);
    if (index === -1) {
      return;
    }

    this.panels_.splice(index, 1);
    panel.cancelCallback = null;
  }

  /**
   * Obtains item by ID.
   * @param id ID of progress item.
   * @return Progress center item having the
   *     specified ID. Null if the item is not found.
   */
  getItemById(id: string): ProgressCenterItem|undefined {
    return this.items_[this.getItemIndex_(id)];
  }

  /**
   * Obtains item index that have the specifying ID.
   * @param id Item ID.
   * @return Item index. Returns -1 If the item is not found.
   */
  private getItemIndex_(id: string): number {
    for (const [i, item] of this.items_.entries()) {
      if (item.id === id) {
        return i;
      }
    }
    return -1;
  }

  /**
   * Requests all panels to dismiss an error item.
   * @param id Item ID.
   */
  private dismissErrorItem_(id: string) {
    const index = this.getItemIndex_(id);
    if (index > -1) {
      this.items_.splice(index, 1);
    }

    for (const panelItem of this.panels_) {
      panelItem.dismissErrorItem(id);
    }
  }

  /**
   * Testing method to construct a new notification panel item.
   * @param props partial properties from the `ProgressCenterItem`.
   */
  private constructTestItem_(props: Partial<ProgressCenterItem> = {}):
      ProgressCenterItem {
    const item = new ProgressCenterItem();
    const defaults = {
      id: Math.ceil(Math.random() * 10000).toString(),
      itemCount: Math.ceil(Math.random() * 5),
      sourceMessage: 'fake_file.test',
      destinationMessage: 'Downloads',
      type: ProgressItemType.COPY,
      progressMax: 100,
    };
    // Apply defaults and overrides.
    Object.assign(item, defaults, props);

    return item;
  }

  /**
   * Testing method to add the notification panel item to the notification
   * panel.
   * @param item the panel item to be added.
   */
  private addItemToPanel_(item: ProgressCenterItem) {
    this.panels_[0]!.setTimingForTests(
        // Make notification panel item show immediately.
        0,
        // Make notification panel item keep showing for 5 minutes.
        5 * 60 * 1000);
    // Add the item to the panel.
    this.items_.push(item);
    this.updateItem(item);
  }

  /**
   * Testing method to add a new "progressing" state notification panel item.
   * @param props partial properties from the `ProgressCenterItem`.
   */
  protected addProcessingTestItem_(props: Partial<ProgressCenterItem> = {}) {
    const item = this.constructTestItem_({
      state: ProgressItemState.PROGRESSING,
      progressValue: Math.ceil(Math.random() * 90),
      remainingTime: 150,
      ...props,
    });
    this.addItemToPanel_(item);
    return item;
  }

  /**
   * Testing method to add a new "completed" state notification panel item.
   * @param props partial properties from the `ProgressCenterItem`.
   */
  protected addCompletedTestItem_(props: Partial<ProgressCenterItem> = {}) {
    const item = this.constructTestItem_({
      state: ProgressItemState.COMPLETED,
      progressValue: 100,
      ...props,
    });
    // Completed item needs to be in the panel before it completes.
    const oldItem = item.clone();
    oldItem.state = ProgressItemState.PROGRESSING;
    this.panels_[0]?.updateItem(oldItem);
    this.addItemToPanel_(item);
    return item;
  }

  /**
   * Testing method to add a new "error" state notification panel item.
   * @param props partial properties from the `ProgressCenterItem`.
   */
  protected addErrorTestItem_(props: Partial<ProgressCenterItem> = {}) {
    const item = this.constructTestItem_({
      state: ProgressItemState.ERROR,
      message: 'Something went wrong. This is a very long error message.',
      ...props,
    });
    item.extraButton.set(ProgressItemState.ERROR, {
      text: 'Learn more',
      callback: () => {},
    });
    this.addItemToPanel_(item);
    return item;
  }

  /**
   * Testing method to add a new "scanning" state notification panel item.
   * @param props partial properties from the `ProgressCenterItem`.
   */
  protected addScanningTestItem_(props: Partial<ProgressCenterItem> = {}) {
    const item = this.constructTestItem_({
      state: ProgressItemState.SCANNING,
      progressValue: Math.ceil(Math.random() * 90),
      remainingTime: 100,
      ...props,
    });
    // Scanning item needs to be in the panel before it starts to scan.
    const oldItem = item.clone();
    this.panels_[0]?.updateItem(oldItem);
    this.addItemToPanel_(item);
    return item;
  }

  /**
   * Testing method to add a new "paused" state notification panel item.
   * @param props partial properties from the `ProgressCenterItem`.
   */
  protected addPausedTestItem_(props: Partial<ProgressCenterItem> = {}) {
    const item = this.constructTestItem_({
      state: ProgressItemState.PAUSED,
      ...props,
    });
    // Paused item needs to be in the panel before it pauses.
    const oldItem = item.clone();
    this.panels_[0]?.updateItem(oldItem);
    this.addItemToPanel_(item);
    return item;
  }
}