chromium/chrome/browser/resources/downloads/item.ts

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

import './icons.html.js';
// <if expr="_google_chrome">
import './internal/icons.html.js';
// </if>
import 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.js';
import 'chrome://resources/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/cr_elements/cr_icons.css.js';
import 'chrome://resources/cr_elements/cr_hidden_style.css.js';
// <if expr="_google_chrome">
import 'chrome://resources/cr_elements/cr_link_row/cr_link_row.js';
// </if>
import 'chrome://resources/cr_elements/cr_progress/cr_progress.js';
import 'chrome://resources/cr_elements/icons.html.js';
import 'chrome://resources/cr_elements/cr_shared_vars.css.js';
import 'chrome://resources/js/action_link.js';
import 'chrome://resources/cr_elements/action_link.css.js';
import './strings.m.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';

import {getInstance as getAnnouncerInstance} from 'chrome://resources/cr_elements/cr_a11y_announcer/cr_a11y_announcer.js';
import type {CrActionMenuElement} from 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.js';
import type {CrIconButtonElement} from 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import {getToastManager} from 'chrome://resources/cr_elements/cr_toast/cr_toast_manager.js';
import {FocusRowMixin} from 'chrome://resources/cr_elements/focus_row_mixin.js';
import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
import {assert, assertNotReached} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {mojoString16ToString} from 'chrome://resources/js/mojo_type_util.js';
import {sanitizeInnerHtml} from 'chrome://resources/js/parse_html_subset.js';
import {htmlEscape} from 'chrome://resources/js/util.js';
import type {String16} from 'chrome://resources/mojo/mojo/public/mojom/base/string16.mojom-webui.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {BrowserProxy} from './browser_proxy.js';
import type {MojomData} from './data.js';
import type {PageHandlerInterface} from './downloads.mojom-webui.js';
import {DangerType, SafeBrowsingState, State, TailoredWarningType} from './downloads.mojom-webui.js';
import {IconLoaderImpl} from './icon_loader.js';
import {getTemplate} from './item.html.js';

export interface DownloadsItemElement {
  $: {
    'controlled-by': HTMLElement,
    'file-icon': HTMLImageElement,
    'file-link': HTMLAnchorElement,
    'referrer-url': HTMLAnchorElement,
    'url': HTMLAnchorElement,
  };
}

const DownloadsItemElementBase = I18nMixin(FocusRowMixin(PolymerElement));

/**
 * The UI pattern for displaying a download. Computed from DangerType and other
 * properties of the download and user's profile.
 */
enum DisplayType {
  NORMAL,
  DANGEROUS,
  SUSPICIOUS,
  UNVERIFIED,
  INSECURE,
  ERROR,
}

export class DownloadsItemElement extends DownloadsItemElementBase {
  static get is() {
    return 'downloads-item';
  }

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

  static get properties() {
    return {
      data: Object,

      completelyOnDisk_: {
        computed: 'computeCompletelyOnDisk_(' +
            'data.state, data.fileExternallyRemoved)',
        type: Boolean,
        value: true,
      },

      shouldLinkFilename_: {
        computed: 'computeShouldLinkFilename_(' +
            'data.dangerType, completelyOnDisk_)',
        type: Boolean,
        value: true,
      },

      hasShowInFolderLink_: {
        computed: 'computeHasShowInFolderLink_(' +
            'data.state, data.fileExternallyRemoved, data.dangerType)',
        type: Boolean,
        value: true,
      },

      controlledBy_: {
        computed: 'computeControlledBy_(data.byExtId, data.byExtName)',
        type: String,
        value: '',
      },

      iconAriaLabel_: {
        type: String,
        computed: 'computeIconAriaLabel_(displayType_)',
      },

      isActive_: {
        computed: 'computeIsActive_(data.fileExternallyRemoved, completelyOnDisk_)',
        type: Boolean,
        value: true,
      },

      isDangerous_: {
        computed: 'computeIsDangerous_(data.state)',
        type: Boolean,
        value: false,
      },

      isMalware_: {
        computed: 'computeIsMalware_(isDangerous_, data.dangerType)',
        type: Boolean,
        value: false,
      },

      isReviewable_: {
        computed: 'computeIsReviewable_(data.isReviewable)',
        type: Boolean,
        value: false,
      },

      isInProgress_: {
        computed: 'computeIsInProgress_(data.state)',
        type: Boolean,
        value: false,
      },

      pauseOrResumeText_: {
        computed: 'computePauseOrResumeText_(isInProgress_, data.resume)',
        type: String,
      },

      showCancel_: {
        computed: 'computeShowCancel_(data.state)',
        type: Boolean,
        value: false,
      },

      showProgress_: {
        computed: 'computeShowProgress_(showCancel_, data.percent)',
        type: Boolean,
        value: false,
      },

      showDeepScan_: {
        computed: 'computeShowDeepScan_(data.state)',
        type: Boolean,
        value: false,
      },

      showOpenAnyway_: {
        computed: 'computeShowOpenAnyway_(data.dangerType)',
        type: Boolean,
        value: false,
      },

      displayType_: {
        computed: 'computeDisplayType_(data.isInsecure, data.state,' +
            'data.dangerType, data.safeBrowsingState,' +
            'data.hasSafeBrowsingVerdict)',
        type: DisplayType,
        value: DisplayType.NORMAL,
      },

      // <if expr="_google_chrome">
      showEsbPromotion: {
        type: Boolean,
        value: false,
      },
      // </if>

      useFileIcon_: Boolean,

      showReferrerUrl_: {
        type: Boolean,
        value: () => loadTimeData.getBoolean('showReferrerUrl'),
      },
    };
  }

  static get observers() {
    return [
      // TODO(dbeam): this gets called way more when I observe data.byExtId
      // and data.byExtName directly. Why?
      'observeControlledBy_(controlledBy_)',
      'observeDisplayType_(displayType_, isDangerous_, data.*)',
      'restoreFocusAfterCancelIfNeeded_(data)',
    ];
  }

  data: MojomData;
  // <if expr="_google_chrome">
  showEsbPromotion: boolean;
  // </if>
  private mojoHandler_: PageHandlerInterface|null = null;
  private controlledBy_: string;
  private iconAriaLabel_: string;
  private isActive_: boolean;
  private isDangerous_: boolean;
  private isReviewable_: boolean;
  private isInProgress_: boolean;
  private pauseOrResumeText_: string;
  private showCancel_: boolean;
  private showProgress_: boolean;
  private showDeepScan_: boolean;
  private showOpenAnyway_: boolean;
  private useFileIcon_: boolean;
  private restoreFocusAfterCancel_: boolean = false;
  private displayType_: DisplayType;
  private completelyOnDisk_: boolean;
  override overrideCustomEquivalent: boolean;

  constructor() {
    super();

    /** Used by FocusRowMixin. */
    this.overrideCustomEquivalent = true;
  }

  override ready() {
    super.ready();

    this.setAttribute('role', 'row');
    this.mojoHandler_ = BrowserProxy.getInstance().handler;
  }

  /** Overrides FocusRowMixin. */
  override getCustomEquivalent(sampleElement: HTMLElement): HTMLElement|null {
    if (sampleElement.getAttribute('focus-type') === 'cancel') {
      return this.shadowRoot!.querySelector('[focus-type="retry"]');
    }
    if (sampleElement.getAttribute('focus-type') === 'retry') {
      return this.shadowRoot!.querySelector('[focus-type="pauseOrResume"]');
    }
    return null;
  }

  getFileIcon(): HTMLImageElement {
    return this.$['file-icon'];
  }

  getMoreActionsButton(): CrIconButtonElement|null {
    const button =
        this.shadowRoot!.querySelector<CrIconButtonElement>('#more-actions');
    return button || null;
  }

  getMoreActionsMenu(): CrActionMenuElement {
    const menu = this.shadowRoot!.querySelector<CrActionMenuElement>(
        '#more-actions-menu');
    assert(!!menu);
    return menu;
  }

  /**
   * @return A JS string of the display URL.
   */
  private getDisplayUrlStr_(displayUrl: String16): string {
    return mojoString16ToString(displayUrl);
  }

  private computeClass_(): string {
    const classes = [];

    if (this.isActive_) {
      classes.push('is-active');
    }

    if (this.isDangerous_) {
      classes.push('dangerous');
    }

    if (this.showProgress_) {
      classes.push('show-progress');
    }

    return classes.join(' ');
  }

  private computeCompletelyOnDisk_(): boolean {
    if (this.data.fileExternallyRemoved) {
      return false;
    }
    switch (this.data.state) {
      case State.kComplete:
        return true;
      case State.kInProgress:
      case State.kCancelled:
      case State.kPaused:
      case State.kDangerous:
      case State.kInterrupted:
      case State.kInsecure:
      case State.kAsyncScanning:
      case State.kPromptForScanning:
      case State.kPromptForLocalPasswordScanning:
        return false;
      default:
        assertNotReached('Unhandled State encountered');
    }
  }

  private computeShouldLinkFilename_(): boolean {
    if (this.data === undefined) {
      return false;
    }

    if (!this.completelyOnDisk_) {
      return false;
    }

    switch (this.data.dangerType) {
      case DangerType.kDeepScannedFailed:
        return false;
      case DangerType.kNoApplicableDangerType:
      case DangerType.kDangerousFile:
      case DangerType.kDangerousUrl:
      case DangerType.kDangerousContent:
      case DangerType.kCookieTheft:
      case DangerType.kUncommonContent:
      case DangerType.kDangerousHost:
      case DangerType.kPotentiallyUnwanted:
      case DangerType.kAsyncScanning:
      case DangerType.kAsyncLocalPasswordScanning:
      case DangerType.kBlockedPasswordProtected:
      case DangerType.kBlockedTooLarge:
      case DangerType.kSensitiveContentWarning:
      case DangerType.kSensitiveContentBlock:
      case DangerType.kDeepScannedSafe:
      case DangerType.kDeepScannedOpenedDangerous:
      case DangerType.kBlockedScanFailed:
        return true;
      default:
        assertNotReached('Unhandled DangerType encountered');
    }
  }

  private computeHasShowInFolderLink_(): boolean {
    if (this.data === undefined) {
      return false;
    }

    if (!this.computeCompletelyOnDisk_()) {
      return false;
    }

    switch (this.data.dangerType) {
      case DangerType.kDeepScannedFailed:
        return false;
      case DangerType.kNoApplicableDangerType:
      case DangerType.kDangerousFile:
      case DangerType.kDangerousUrl:
      case DangerType.kDangerousContent:
      case DangerType.kCookieTheft:
      case DangerType.kUncommonContent:
      case DangerType.kDangerousHost:
      case DangerType.kPotentiallyUnwanted:
      case DangerType.kAsyncScanning:
      case DangerType.kAsyncLocalPasswordScanning:
      case DangerType.kBlockedPasswordProtected:
      case DangerType.kBlockedTooLarge:
      case DangerType.kSensitiveContentWarning:
      case DangerType.kSensitiveContentBlock:
      case DangerType.kDeepScannedSafe:
      case DangerType.kDeepScannedOpenedDangerous:
      case DangerType.kBlockedScanFailed:
        return true;
      default:
        assertNotReached('Unhandled DangerType encountered');
    }
  }

  private computeControlledBy_(): string {
    if (!this.data.byExtId || !this.data.byExtName) {
      return '';
    }

    const url = `chrome://extensions/?id=${this.data.byExtId}`;
    const name = this.data.byExtName;
    return loadTimeData.getStringF('controlledByUrl', url, htmlEscape(name));
  }

  private computeDate_(): string {
    assert(typeof this.data.hideDate === 'boolean');
    if (this.data.hideDate) {
      return '';
    }
    return this.data.sinceString || this.data.dateString;
  }

  private computeDescriptionVisible_(): boolean {
    return this.computeDescription_() !== '';
  }

  private computeSecondLineVisible_(): boolean {
    if (!this.data) {
      return false;
    }
    switch (this.data.state) {
      case State.kAsyncScanning:
        return true;
      case State.kInProgress:
      case State.kCancelled:
      case State.kComplete:
      case State.kPaused:
      case State.kDangerous:
      case State.kInterrupted:
      case State.kInsecure:
      case State.kPromptForScanning:
      case State.kPromptForLocalPasswordScanning:
        return false;
      default:
        assertNotReached('Unhandled State encountered');
    }
  }

  private isSuspiciousEnterpriseApVerdict_(
      requestsApVerdicts: boolean, dangerType: DangerType): boolean {
    switch (dangerType) {
      case DangerType.kUncommonContent:
        return requestsApVerdicts;
      case DangerType.kSensitiveContentWarning:
        return true;
      case DangerType.kNoApplicableDangerType:
      case DangerType.kDangerousFile:
      case DangerType.kDangerousUrl:
      case DangerType.kDangerousContent:
      case DangerType.kCookieTheft:
      case DangerType.kDangerousHost:
      case DangerType.kPotentiallyUnwanted:
      case DangerType.kAsyncScanning:
      case DangerType.kAsyncLocalPasswordScanning:
      case DangerType.kBlockedPasswordProtected:
      case DangerType.kBlockedTooLarge:
      case DangerType.kSensitiveContentBlock:
      case DangerType.kDeepScannedFailed:
      case DangerType.kDeepScannedSafe:
      case DangerType.kDeepScannedOpenedDangerous:
      case DangerType.kBlockedScanFailed:
        return false;
      default:
        assertNotReached('Unhandled DangerType encountered');
    }
  }

  private computeDisplayType_(): DisplayType {
    // Most downloads are normal. If we don't have data, don't assume danger.
    if (!this.data) {
      return DisplayType.NORMAL;
    }

    if (this.data.isInsecure) {
      return DisplayType.INSECURE;
    }

    switch (this.data.state) {
      case State.kAsyncScanning:
      case State.kPromptForScanning:
      case State.kPromptForLocalPasswordScanning:
        return DisplayType.SUSPICIOUS;
      case State.kInsecure:
        return DisplayType.INSECURE;
      case State.kInProgress:
      case State.kCancelled:
      case State.kComplete:
      case State.kPaused:
      case State.kDangerous:
      case State.kInterrupted:
        break;
      default:
        assertNotReached('Unhandled State encountered');
    }

    // Enterprise AP verdicts.
    if (this.isSuspiciousEnterpriseApVerdict_(
            loadTimeData.getBoolean('requestsApVerdicts'),
            this.data.dangerType)) {
      return DisplayType.SUSPICIOUS;
    }

    switch (this.data.dangerType) {
      // Mimics logic in download_ui_model.cc for downloads with danger_type
      // DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE.
      case DangerType.kDangerousFile:
        return this.data.hasSafeBrowsingVerdict ? DisplayType.SUSPICIOUS :
                                                  DisplayType.UNVERIFIED;
      case DangerType.kDangerousUrl:
      case DangerType.kDangerousContent:
      case DangerType.kCookieTheft:
      case DangerType.kDangerousHost:
      case DangerType.kPotentiallyUnwanted:
      case DangerType.kDeepScannedOpenedDangerous:
        return DisplayType.DANGEROUS;
      case DangerType.kUncommonContent:
      case DangerType.kDeepScannedFailed:
        return DisplayType.SUSPICIOUS;
      case DangerType.kNoApplicableDangerType:
      case DangerType.kAsyncScanning:
      case DangerType.kAsyncLocalPasswordScanning:
      case DangerType.kSensitiveContentWarning:
      case DangerType.kDeepScannedSafe:
      case DangerType.kBlockedScanFailed:
        return DisplayType.NORMAL;
      case DangerType.kBlockedPasswordProtected:
      case DangerType.kBlockedTooLarge:
      case DangerType.kSensitiveContentBlock:
        return DisplayType.ERROR;
      default:
        assertNotReached('Unhandled DangerType encountered');
    }
  }

  private computeDeepScanControlText_(): string {
    switch (this.data.state) {
      case State.kPromptForScanning:
        return loadTimeData.getString('controlDeepScan');
      case State.kPromptForLocalPasswordScanning:
        return loadTimeData.getString('controlLocalPasswordScan');
      case State.kInProgress:
      case State.kCancelled:
      case State.kComplete:
      case State.kPaused:
      case State.kDangerous:
      case State.kInterrupted:
      case State.kInsecure:
      case State.kAsyncScanning:
        return '';
      default:
        assertNotReached('Unhandled State encountered');
    }
  }

  private computeSaveDangerousLabel_(): string {
    switch (this.displayType_) {
      case DisplayType.DANGEROUS:
        return this.i18n('controlKeepDangerous');
      case DisplayType.SUSPICIOUS:
        return this.i18n('controlKeepSuspicious');
      case DisplayType.UNVERIFIED:
        return this.i18n('controlKeepUnverified');
      case DisplayType.INSECURE:
        return this.i18n('controlKeepInsecure');
      case DisplayType.NORMAL:
      case DisplayType.ERROR:
        return '';
      default:
        assertNotReached('Unhandled DisplayType encountered');
    }
  }

  private computeDescription_(): string {
    if (!this.data) {
      return '';
    }

    const data = this.data;

    switch (data.state) {
      case State.kComplete:
        switch (data.dangerType) {
          case DangerType.kDeepScannedOpenedDangerous:
            return loadTimeData.getString('deepScannedOpenedDangerousDesc');
          case DangerType.kDeepScannedFailed:
            return loadTimeData.getString('deepScannedFailedDesc');
          case DangerType.kNoApplicableDangerType:
          case DangerType.kDangerousFile:
          case DangerType.kDangerousUrl:
          case DangerType.kDangerousContent:
          case DangerType.kCookieTheft:
          case DangerType.kUncommonContent:
          case DangerType.kDangerousHost:
          case DangerType.kPotentiallyUnwanted:
          case DangerType.kAsyncScanning:
          case DangerType.kAsyncLocalPasswordScanning:
          case DangerType.kBlockedPasswordProtected:
          case DangerType.kBlockedTooLarge:
          case DangerType.kSensitiveContentWarning:
          case DangerType.kSensitiveContentBlock:
          case DangerType.kDeepScannedSafe:
          case DangerType.kBlockedScanFailed:
            return '';
          default:
            assertNotReached('Unhandled DangerType encountered');
        }
      case State.kInsecure:
        return loadTimeData.getString('insecureDownloadDesc');
      case State.kDangerous:
        switch (data.dangerType) {
          case DangerType.kNoApplicableDangerType:
            return '';
          case DangerType.kDangerousFile:
            return data.safeBrowsingState ===
                    SafeBrowsingState.kNoSafeBrowsing ?
                loadTimeData.getString('noSafeBrowsingDesc') :
                loadTimeData.getString('dangerFileDesc');
          case DangerType.kDangerousUrl:
          case DangerType.kDangerousContent:
          case DangerType.kDangerousHost:
            return loadTimeData.getString('dangerDownloadDesc');
          case DangerType.kCookieTheft:
            switch (data.tailoredWarningType) {
              case TailoredWarningType.kCookieTheft:
                return loadTimeData.getString('dangerDownloadCookieTheft');
              case TailoredWarningType.kCookieTheftWithAccountInfo:
                return data.accountEmail ?
                    loadTimeData.getStringF(
                        'dangerDownloadCookieTheftAndAccountDesc',
                        data.accountEmail) :
                    loadTimeData.getString('dangerDownloadCookieTheft');
              case TailoredWarningType.kSuspiciousArchive:
              case TailoredWarningType.kNoApplicableTailoredWarningType:
                return loadTimeData.getString('dangerDownloadDesc');
              default:
                assertNotReached('Unhandled TailoredWarningType encountered');
            }
          case DangerType.kUncommonContent:
            switch (data.tailoredWarningType) {
              case TailoredWarningType.kSuspiciousArchive:
                return loadTimeData.getString(
                    'dangerUncommonSuspiciousArchiveDesc');
              case TailoredWarningType.kCookieTheft:
              case TailoredWarningType.kCookieTheftWithAccountInfo:
              case TailoredWarningType.kNoApplicableTailoredWarningType:
                return loadTimeData.getString('dangerUncommonDesc');
              default:
                assertNotReached('Unhandled TailoredWarningType encountered');
            }
          case DangerType.kPotentiallyUnwanted:
            return loadTimeData.getString('dangerSettingsDesc');
          case DangerType.kAsyncScanning:
          case DangerType.kAsyncLocalPasswordScanning:
          case DangerType.kBlockedPasswordProtected:
          case DangerType.kBlockedTooLarge:
            return '';
          case DangerType.kSensitiveContentWarning:
            return loadTimeData.getString('sensitiveContentWarningDesc');
          case DangerType.kSensitiveContentBlock:
          case DangerType.kDeepScannedFailed:
          case DangerType.kDeepScannedSafe:
          case DangerType.kDeepScannedOpenedDangerous:
          case DangerType.kBlockedScanFailed:
            return '';
          default:
            assertNotReached('Unhandled DangerType encountered');
        }
      case State.kAsyncScanning:
        return loadTimeData.getString('asyncScanningDownloadDesc');
      case State.kPromptForScanning:
        return loadTimeData.getString('promptForScanningDesc');
      case State.kPromptForLocalPasswordScanning:
        return loadTimeData.getString('promptForLocalPasswordScanningDesc');
      case State.kInProgress:
      case State.kPaused:  // Fallthrough.
        return data.progressStatusText;
      case State.kInterrupted:
        switch (data.dangerType) {
          case DangerType.kNoApplicableDangerType:
          case DangerType.kDangerousFile:
          case DangerType.kDangerousUrl:
          case DangerType.kDangerousContent:
          case DangerType.kDangerousHost:
          case DangerType.kCookieTheft:
          case DangerType.kUncommonContent:
          case DangerType.kPotentiallyUnwanted:
          case DangerType.kAsyncScanning:
          case DangerType.kAsyncLocalPasswordScanning:
            return '';
          case DangerType.kBlockedPasswordProtected:
            return loadTimeData.getString('blockedPasswordProtectedDesc');
          case DangerType.kBlockedTooLarge:
            return loadTimeData.getString('blockedTooLargeDesc');
          case DangerType.kSensitiveContentWarning:
            return '';
          case DangerType.kSensitiveContentBlock:
            return loadTimeData.getString('sensitiveContentBlockedDesc');
          case DangerType.kDeepScannedFailed:
          case DangerType.kDeepScannedSafe:
          case DangerType.kDeepScannedOpenedDangerous:
          case DangerType.kBlockedScanFailed:
            return '';
          default:
            assertNotReached('Unhandled DangerType encountered');
        }
      case State.kCancelled:
        return '';
      default:
        assertNotReached('Unhandled State encountered');
    }
  }

  private computeIconAriaHidden_(): string {
    return (this.iconAriaLabel_ === '').toString();
  }

  private computeIconAriaLabel_(): string {
    switch (this.displayType_) {
      case DisplayType.DANGEROUS:
        return this.i18n('accessibleLabelDangerous');
      case DisplayType.INSECURE:
        return this.i18n('accessibleLabelInsecure');
      case DisplayType.UNVERIFIED:
        return this.i18n('accessibleLabelUnverified');
      case DisplayType.SUSPICIOUS:
        return this.i18n('accessibleLabelSuspicious');
      case DisplayType.NORMAL:
      case DisplayType.ERROR:
        return '';
      default:
        assertNotReached('Unhandled DisplayType encountered');
    }
  }

  private iconAndDescriptionColor_(): string {
    switch (this.displayType_) {
      case DisplayType.DANGEROUS:
      case DisplayType.ERROR:
        return 'red';
      case DisplayType.INSECURE:
      case DisplayType.UNVERIFIED:
      case DisplayType.SUSPICIOUS:
        return 'grey';
      case DisplayType.NORMAL:
        return '';
      default:
        assertNotReached('Unhandled DisplayType encountered');
    }
  }

  private computeIcon_(): string {
    if (this.data) {
      switch (this.displayType_) {
        case DisplayType.DANGEROUS:
          return 'downloads:dangerous';
        case DisplayType.INSECURE:
        case DisplayType.UNVERIFIED:
        case DisplayType.SUSPICIOUS:
          return 'cr:warning';
        case DisplayType.ERROR:
          return 'cr:error';
        case DisplayType.NORMAL:
          break;
        default:
          assertNotReached('Unhandled DisplayType encountered');
      }

      assert(this.displayType_ === DisplayType.NORMAL);
      const dangerType = this.data.dangerType as DangerType;
      if (this.isSuspiciousEnterpriseApVerdict_(
              loadTimeData.getBoolean('requestsApVerdicts'), dangerType)) {
        return 'cr:warning';
      }

      switch (dangerType) {
        case DangerType.kDeepScannedFailed:
          return 'cr:info';
        case DangerType.kSensitiveContentBlock:
        case DangerType.kBlockedTooLarge:
        case DangerType.kBlockedPasswordProtected:
          return 'cr:error';
        case DangerType.kNoApplicableDangerType:
        case DangerType.kDangerousFile:
        case DangerType.kDangerousUrl:
        case DangerType.kDangerousContent:
        case DangerType.kCookieTheft:
        case DangerType.kUncommonContent:
        case DangerType.kDangerousHost:
        case DangerType.kPotentiallyUnwanted:
        case DangerType.kAsyncScanning:
        case DangerType.kAsyncLocalPasswordScanning:
        case DangerType.kSensitiveContentWarning:
        case DangerType.kDeepScannedSafe:
        case DangerType.kDeepScannedOpenedDangerous:
        case DangerType.kBlockedScanFailed:
          break;
        default:
          assertNotReached('Unhandled DangerType encountered');
      }

      switch (this.data.state) {
        case State.kAsyncScanning:
        case State.kPromptForScanning:
        case State.kPromptForLocalPasswordScanning:
          return 'cr:warning';
        case State.kInProgress:
        case State.kCancelled:
        case State.kComplete:
        case State.kPaused:
        case State.kDangerous:
        case State.kInterrupted:
        case State.kInsecure:
          break;
        default:
          assertNotReached('Unhandled State encountered');
      }
    }
    if (this.isDangerous_) {
      return 'downloads:dangerous';
    }
    if (!this.useFileIcon_) {
      return 'cr:insert-drive-file';
    }
    return '';
  }

  private computeIconColor_(): string {
    if (this.data) {
      return this.iconAndDescriptionColor_();
    }
    if (this.isDangerous_) {
      return 'red';
    }
    if (!this.useFileIcon_) {
      return 'light-grey';
    }
    return '';
  }

  private computeIsActive_(): boolean {
    if (this.data && this.data.fileExternallyRemoved) {
      return false;
    }
    return this.completelyOnDisk_;
  }

  private computeIsDangerous_(): boolean {
    switch (this.data.state) {
      case State.kDangerous:
      case State.kInsecure:
        return true;
      case State.kInProgress:
      case State.kCancelled:
      case State.kComplete:
      case State.kPaused:
      case State.kInterrupted:
      case State.kAsyncScanning:
      case State.kPromptForScanning:
      case State.kPromptForLocalPasswordScanning:
        return false;
      default:
        assertNotReached('Unhandled State encountered');
    }
  }

  private computeIsInProgress_(): boolean {
    switch (this.data.state) {
      case State.kInProgress:
        return true;
      case State.kCancelled:
      case State.kComplete:
      case State.kPaused:
      case State.kDangerous:
      case State.kInterrupted:
      case State.kInsecure:
      case State.kAsyncScanning:
      case State.kPromptForScanning:
      case State.kPromptForLocalPasswordScanning:
        return false;
      default:
        assertNotReached('Unhandled State encountered');
    }
  }

  private computeIsMalware_(): boolean {
    if (!this.isDangerous_) {
      return false;
    }

    switch (this.data.dangerType) {
      case DangerType.kDangerousUrl:
      case DangerType.kDangerousContent:
      case DangerType.kCookieTheft:
      case DangerType.kDangerousHost:
      case DangerType.kPotentiallyUnwanted:
        return true;
      case DangerType.kNoApplicableDangerType:
      case DangerType.kDangerousFile:
      case DangerType.kUncommonContent:
      case DangerType.kAsyncScanning:
      case DangerType.kAsyncLocalPasswordScanning:
      case DangerType.kBlockedPasswordProtected:
      case DangerType.kBlockedTooLarge:
      case DangerType.kSensitiveContentWarning:
      case DangerType.kSensitiveContentBlock:
      case DangerType.kDeepScannedFailed:
      case DangerType.kDeepScannedSafe:
      case DangerType.kDeepScannedOpenedDangerous:
      case DangerType.kBlockedScanFailed:
        return false;
      default:
        assertNotReached('Unhandled DangerType encountered');
    }
  }

  private computeIsReviewable_(): boolean {
    return this.data.isReviewable;
  }

  private computePauseOrResumeText_(): string {
    if (this.data === undefined) {
      return '';
    }

    if (this.isInProgress_) {
      return loadTimeData.getString('controlPause');
    }
    if (this.data.resume) {
      return loadTimeData.getString('controlResume');
    }
    return '';
  }

  private computeShowRemove_(): boolean {
    const canDelete = loadTimeData.getBoolean('allowDeletingHistory');
    const hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete;
    return !hideRemove;
  }

  private computeRemoveStyle_(): string {
    return this.computeShowRemove_() ? '' : 'visibility: hidden';
  }

  private computeShowControlsForDangerous_(): boolean {
    return !this.isReviewable_ && this.isDangerous_;
  }

  private computeShowCancel_(): boolean {
    if (!this.data) {
      return false;
    }
    switch (this.data.state) {
      case State.kInProgress:
      case State.kPaused:
        return true;
      case State.kCancelled:
      case State.kComplete:
      case State.kDangerous:
      case State.kInterrupted:
      case State.kInsecure:
      case State.kAsyncScanning:
      case State.kPromptForScanning:
      case State.kPromptForLocalPasswordScanning:
        return false;
      default:
        assertNotReached('Unhandled State encountered');
    }
  }

  private computeShowProgress_(): boolean {
    if (!this.data) {
      return false;
    }
    switch (this.data.state) {
      case State.kInProgress:
      case State.kCancelled:
      case State.kComplete:
      case State.kPaused:
      case State.kDangerous:
      case State.kInterrupted:
      case State.kInsecure:
        return this.showCancel_ && this.data.percent >= -1;
      case State.kAsyncScanning:
        return true;
      case State.kPromptForScanning:
      case State.kPromptForLocalPasswordScanning:
        return false;
      default:
        assertNotReached('Unhandled State encountered');
    }
  }

  private computeShowDeepScan_(): boolean {
    switch (this.data.state) {
      case State.kPromptForScanning:
      case State.kPromptForLocalPasswordScanning:
        return true;
      case State.kInProgress:
      case State.kCancelled:
      case State.kComplete:
      case State.kPaused:
      case State.kDangerous:
      case State.kInterrupted:
      case State.kInsecure:
      case State.kAsyncScanning:
        return false;
      default:
        assertNotReached('Unhandled State encountered');
    }
  }

  private computeShowOpenAnyway_(): boolean {
    switch (this.data.dangerType) {
      case DangerType.kDeepScannedFailed:
        return true;
      case DangerType.kNoApplicableDangerType:
      case DangerType.kDangerousFile:
      case DangerType.kDangerousUrl:
      case DangerType.kDangerousContent:
      case DangerType.kCookieTheft:
      case DangerType.kUncommonContent:
      case DangerType.kDangerousHost:
      case DangerType.kPotentiallyUnwanted:
      case DangerType.kAsyncScanning:
      case DangerType.kAsyncLocalPasswordScanning:
      case DangerType.kBlockedPasswordProtected:
      case DangerType.kBlockedTooLarge:
      case DangerType.kSensitiveContentWarning:
      case DangerType.kSensitiveContentBlock:
      case DangerType.kDeepScannedSafe:
      case DangerType.kDeepScannedOpenedDangerous:
      case DangerType.kBlockedScanFailed:
        return false;
      default:
        assertNotReached('Unhandled DangerType encountered');
    }
  }

  private computeShowActionMenu_(): boolean {
    if (!this.data) {
      return false;
    }
    // If any of these actions are available, the action menu must be shown
    // because they don't have corresponding "quick actions".
    return !!this.pauseOrResumeText_ ||             // pause-or-resume
        this.computeShowControlsForDangerous_() ||  // save-dangerous
        this.data.retry ||                          // retry
        this.showDeepScan_ ||    // deep-scan and bypass-deep-scan
        this.showCancel_ ||      // cancel
        this.showOpenAnyway_ ||  // open-anyway
        this.isReviewable_;      // review-dangerous
  }

  private computeShowCopyDownloadLink_(): boolean {
    return !!(this.data && this.data.url);
  }

  private computeShowQuickRemove_(): boolean {
    return this.isReviewable_ || this.computeShowRemove_() ||
        this.computeShowControlsForDangerous_();
  }

  private computeShowQuickShow_(): boolean {
    // Only show the quick "show in folder" button if the full action menu
    // is hidden. If the action menu is shown, hide the quick "show in folder"
    // button to save space since this action will have an entry in the menu
    // anyway.
    return this.computeHasShowInFolderLink_() && !this.computeShowActionMenu_();
  }

  private computeTag_(): string {
    switch (this.data.state) {
      case State.kCancelled:
        return loadTimeData.getString('statusCancelled');
      case State.kInterrupted:
        return this.data.lastReasonText;
      case State.kComplete:
        return this.data.fileExternallyRemoved ?
            loadTimeData.getString('statusRemoved') :
            '';
      case State.kInProgress:
      case State.kPaused:
      case State.kDangerous:
      case State.kInsecure:
      case State.kAsyncScanning:
      case State.kPromptForScanning:
      case State.kPromptForLocalPasswordScanning:
        return '';
      default:
        assertNotReached('Unhandled State encountered');
    }
  }

  private isIndeterminate_(): boolean {
    if (this.data.percent === -1) {
      return true;
    }
    switch (this.data.state) {
      case State.kAsyncScanning:
        return true;
      case State.kInProgress:
      case State.kCancelled:
      case State.kComplete:
      case State.kPaused:
      case State.kDangerous:
      case State.kInterrupted:
      case State.kInsecure:
      case State.kPromptForScanning:
      case State.kPromptForLocalPasswordScanning:
        return false;
      default:
        assertNotReached('Unhandled State encountered');
    }
  }

  private observeControlledBy_() {
    this.$['controlled-by'].innerHTML = sanitizeInnerHtml(this.controlledBy_);
    if (this.controlledBy_) {
      const link = this.shadowRoot!.querySelector('#controlled-by a');
      link!.setAttribute('focus-row-control', '');
      link!.setAttribute('focus-type', 'controlledBy');
    }
  }

  private shouldShowReferrerUrl_(): boolean {
    return loadTimeData.getBoolean('showReferrerUrl') &&
        this.data.displayReferrerUrl.data.length > 0;
  }

  getReferrerUrlAnchorElement(): HTMLAnchorElement|null {
    return this.$['referrer-url'].querySelector('a') || null;
  }

  private observeDisplayType_() {
    const removeFileUrlLinks = () => {
      this.$.url.removeAttribute('href');
      this.$['file-link'].removeAttribute('href');
    };

    const updateReferrerUrlLinkHref = (hrefValue?: string) => {
      const referrerUrlLink = this.getReferrerUrlAnchorElement();
      if (!referrerUrlLink) {
        // No <a> tag, nothing to do.
        return;
      }
      if (!hrefValue) {
        referrerUrlLink.removeAttribute('href');
        return;
      }
      referrerUrlLink.setAttribute('href', hrefValue);
      referrerUrlLink.setAttribute('focus-row-control', '');
      referrerUrlLink.setAttribute('focus-type', 'referrerUrl');
      referrerUrlLink.setAttribute('target', '_blank');
      referrerUrlLink.setAttribute('rel', 'noopener');
    };

    if (!this.data) {
      return;
    }

    // "else" case already handled by `shouldShowReferrerUrl_`.
    if (this.data.displayReferrerUrl.data.length > 0) {
      const referrerLine = loadTimeData.getStringF(
          'referrerLine', mojoString16ToString(this.data.displayReferrerUrl));
      this.$['referrer-url'].innerHTML = sanitizeInnerHtml(referrerLine);
    }

    // Returns whether to use the file icon, and additionally clears file url
    // links if necessary.
    const mayUseFileIcon = () => {
      const use = this.displayType_ === DisplayType.NORMAL;
      if (!use) {
        removeFileUrlLinks();
        updateReferrerUrlLinkHref();
      }
      return use;
    };

    this.useFileIcon_ = mayUseFileIcon();
    if (!this.useFileIcon_) {
      return;
    }

    // The file is not dangerous. Link the url if supplied.
    if (this.data.url) {
      this.$.url.href = this.data.url.url;
    } else {
      removeFileUrlLinks();
    }

    // The file is not dangerous. Link the referrer_url if supplied.
    if (this.data.referrerUrl) {
      updateReferrerUrlLinkHref(this.data.referrerUrl.url);
    } else {
      updateReferrerUrlLinkHref();
    }

    const path = this.data.filePath;
    IconLoaderImpl.getInstance()
        .loadIcon(this.$['file-icon'], path)
        .then(success => {
          if (path === this.data.filePath &&
              this.data.state !== State.kAsyncScanning) {
            // Check again if we may use the file icon, to avoid a race between
            // loading the icon and determining the proper danger type.
            this.useFileIcon_ = mayUseFileIcon() && success;
          }
        });
  }

  // <if expr="_google_chrome">
  private onEsbPromotionClick_() {
    assert(!!this.mojoHandler_);
    this.mojoHandler_.openEsbSettings();
  }
  // </if>

  private onCopyDownloadLinkClick_(e: Event) {
    if (!this.data.url) {
      return;
    }
    navigator.clipboard.writeText(this.data.url.url);
    this.displayCopyToast_(e);
  }

  private onMoreActionsClick_() {
    const button = this.getMoreActionsButton();
    // The menu button is not always shown, but if this handler is invoked, then
    // it must be.
    assert(!!button);
    this.getMoreActionsMenu().showAt(button);
  }

  // Handles the "x" remove button which can be different actions depending on
  // the state of the download.
  private onQuickRemoveClick_(e: Event) {
    if (this.isReviewable_ || this.computeShowControlsForDangerous_()) {
      this.onDiscardDangerousClick_(e);
      return;
    }
    assert(this.computeShowRemove_());
    this.onRemoveClick_(e);
  }

  private onCancelClick_() {
    this.restoreFocusAfterCancel_ = true;
    assert(!!this.mojoHandler_);
    this.mojoHandler_.cancel(this.data.id);
    getAnnouncerInstance().announce(
        loadTimeData.getString('screenreaderCanceled'));
    this.getMoreActionsMenu().close();
  }

  private onDiscardDangerousClick_(e: Event) {
    assert(!!this.mojoHandler_);
    this.mojoHandler_.discardDangerous(this.data.id);
    this.displayRemovedToast_(/*canUndo=*/ false, e);
    this.getMoreActionsMenu().close();
  }

  private onOpenNowClick_() {
    this.mojoHandler_!.openDuringScanningRequiringGesture(this.data.id);
    this.getMoreActionsMenu().close();
  }

  private onDeepScanClick_() {
    this.mojoHandler_!.deepScan(this.data.id);
    this.getMoreActionsMenu().close();
  }

  private onBypassDeepScanClick_() {
    this.mojoHandler_!.bypassDeepScanRequiringGesture(this.data.id);
    this.getMoreActionsMenu().close();
  }

  private onReviewDangerousClick_() {
    this.mojoHandler_!.reviewDangerousRequiringGesture(this.data.id);
    this.getMoreActionsMenu().close();
  }

  private onOpenAnywayClick_() {
    this.mojoHandler_!.openFileRequiringGesture(this.data.id);
    this.getMoreActionsMenu().close();
  }

  private onDragStart_(e: Event) {
    e.preventDefault();
    this.mojoHandler_!.drag(this.data.id);
  }

  private onFileLinkClick_(e: Event) {
    e.preventDefault();
    this.mojoHandler_!.openFileRequiringGesture(this.data.id);
  }

  private onUrlClick_() {
    if (!this.data.url) {
      return;
    }
    chrome.send(
        'metricsHandler:recordAction', ['Downloads_OpenUrlOfDownloadedItem']);
  }

  private doPause_() {
    assert(!!this.mojoHandler_);
    this.mojoHandler_.pause(this.data.id);
    getAnnouncerInstance().announce(
        loadTimeData.getString('screenreaderPaused'));
  }

  private doResume_() {
    assert(!!this.mojoHandler_);
    this.mojoHandler_.resume(this.data.id);
    getAnnouncerInstance().announce(
        loadTimeData.getString('screenreaderResumed'));
  }

  private onPauseOrResumeClick_() {
    if (this.isInProgress_) {
      this.doPause_();
    } else {
      this.doResume_();
    }
    this.getMoreActionsMenu().close();
  }

  private displayCopyToast_(e: Event) {
    if (!this.data.url) {
      return;
    }

    const pieces = loadTimeData.getSubstitutedStringPieces(
                       loadTimeData.getString('toastCopiedDownloadLink'),
                       this.data.url.url) as unknown as
        Array<{collapsible: boolean, value: string, arg: string}>;
    pieces.forEach(p => {
      p.collapsible = !!p.arg;
    });
    getToastManager().showForStringPieces(pieces, /*hideSlotted=*/ true);

    e.stopPropagation();
    e.preventDefault();
  }

  private displayRemovedToast_(canUndo: boolean, e: Event) {
    const templateStringId =
        (this.displayType_ === DisplayType.NORMAL && this.completelyOnDisk_) ?
        'toastDeletedFromHistoryStillOnDevice' :
        'toastDeletedFromHistory';
    const pieces =
        loadTimeData.getSubstitutedStringPieces(
            loadTimeData.getString(templateStringId), this.data.fileName) as
        unknown as Array<{collapsible: boolean, value: string, arg?: string}>;

    pieces.forEach(p => {
      // Make the file name collapsible.
      p.collapsible = !!p.arg;
    });
    getToastManager().showForStringPieces(pieces, /*hideSlotted=*/ !canUndo);

    // Stop propagating a click to the document to remove toast.
    e.stopPropagation();
    e.preventDefault();
  }

  private onRemoveClick_(e: Event) {
    assert(!!this.mojoHandler_);
    this.mojoHandler_.remove(this.data.id);
    const canUndo = !this.data.isDangerous && !this.data.isInsecure;
    this.displayRemovedToast_(canUndo, e);
    this.getMoreActionsMenu().close();
  }

  private onRetryClick_() {
    this.mojoHandler_!.retryDownload(this.data.id);
    this.getMoreActionsMenu().close();
  }

  private notifySaveDangerousClick_() {
    this.dispatchEvent(new CustomEvent('save-dangerous-click', {
      bubbles: true,
      composed: true,
      detail: {id: this.data.id},
    }));
  }

  private onSaveDangerousClick_() {
    this.getMoreActionsMenu().close();

    if (this.displayType_ === DisplayType.DANGEROUS) {
      this.notifySaveDangerousClick_();
      return;
    }

    // "Suspicious" types which show up in grey can be validated directly.
    // This maps each such display type to its applicable screenreader
    // announcement string id.
    const SAVED_FROM_PAGE_TYPES_ANNOUNCEMENTS = new Map([
      [DisplayType.SUSPICIOUS, 'screenreaderSavedSuspicious'],
      [DisplayType.UNVERIFIED, 'screenreaderSavedUnverified'],
      [DisplayType.INSECURE, 'screenreaderSavedInsecure'],
    ]);
    assert(SAVED_FROM_PAGE_TYPES_ANNOUNCEMENTS.has(this.displayType_));
    assert(!!this.mojoHandler_);
    this.mojoHandler_.saveSuspiciousRequiringGesture(this.data.id);
    const announcement = loadTimeData.getString(
        SAVED_FROM_PAGE_TYPES_ANNOUNCEMENTS.get(this.displayType_) as string);
    getAnnouncerInstance().announce(announcement);
  }

  private onShowClick_() {
    this.mojoHandler_!.show(this.data.id);
    this.getMoreActionsMenu().close();
  }

  private restoreFocusAfterCancelIfNeeded_() {
    if (!this.restoreFocusAfterCancel_) {
      return;
    }
    this.restoreFocusAfterCancel_ = false;
    setTimeout(() => {
      const element = this.getFocusRow().getFirstFocusable('retry');
      if (element) {
        (element as HTMLElement).focus();
      }
    });
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'downloads-item': DownloadsItemElement;
  }
}

customElements.define(DownloadsItemElement.is, DownloadsItemElement);