chromium/chrome/browser/resources/ash/settings/os_apps_page/os_apps_page.ts

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

/**
 * @fileoverview
 * 'os-settings-apps-page' is the settings page containing app related settings.
 *
 */
import 'chrome://resources/ash/common/cr_elements/localized_link/localized_link.js';
import 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_link_row/cr_link_row.js';
import 'chrome://resources/ash/common/cr_elements/policy/cr_policy_pref_indicator.js';
import '../controls/settings_dropdown_menu.js';
import '../os_settings_page/os_settings_animated_pages.js';
import '../os_settings_page/os_settings_subpage.js';
import '../os_settings_page/settings_card.js';
import '../settings_shared.css.js';
import '../settings_shared.css.js';
import '../guest_os/guest_os_shared_usb_devices.js';
import '../guest_os/guest_os_shared_paths.js';
import './android_apps_subpage.js';
import './app_notifications_page/app_notifications_subpage.js';
import './app_management_page/app_management_page.js';
import './app_management_page/app_detail_view.js';
import './app_management_page/uninstall_button.js';
import './app_parental_controls/app_setup_pin_dialog.js';
import './app_parental_controls/app_verify_pin_dialog.js';

import {PrefsMixin} from '/shared/settings/prefs/prefs_mixin.js';
import {CrToggleElement} from 'chrome://resources/ash/common/cr_elements/cr_toggle/cr_toggle.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {App} from 'chrome://resources/cr_components/app_management/app_management.mojom-webui.js';
import {AppManagementEntryPoint, AppManagementEntryPointsHistogramName} from 'chrome://resources/cr_components/app_management/constants.js';
import {getAppIcon, getSelectedApp} from 'chrome://resources/cr_components/app_management/util.js';
import {assert} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {AppManagementStoreMixin} from '../common/app_management/store_mixin.js';
import {DeepLinkingMixin} from '../common/deep_linking_mixin.js';
import {androidAppsVisible, isAppParentalControlsFeatureAvailable, isArcVmEnabled, isPlayStoreAvailable, isPluginVmAvailable, isRevampWayfindingEnabled, shouldShowStartup} from '../common/load_time_booleans.js';
import {RouteOriginMixin} from '../common/route_origin_mixin.js';
import {DropdownMenuOptionList} from '../controls/settings_dropdown_menu.js';
import {App as AppWithNotifications, AppNotificationsHandlerInterface, AppNotificationsObserverReceiver, Readiness} from '../mojom-webui/app_notification_handler.mojom-webui.js';
import {AppParentalControlsHandlerInterface} from '../mojom-webui/app_parental_controls_handler.mojom-webui.js';
import {Section} from '../mojom-webui/routes.mojom-webui.js';
import {Setting} from '../mojom-webui/setting.mojom-webui.js';
import {Route, Router, routes} from '../router.js';

import {AndroidAppsBrowserProxyImpl, AndroidAppsInfo} from './android_apps_browser_proxy.js';
import {getAppNotificationProvider} from './app_notifications_page/mojo_interface_provider.js';
import {getAppParentalControlsProvider} from './app_parental_controls/mojo_interface_provider.js';
import {ParentalControlsDialogType, recordParentalControlsDialogFlowCompleted, recordParentalControlsDialogOpened} from './app_parental_controls/metrics_utils.js';
import {getTemplate} from './os_apps_page.html.js';

export function isAppInstalled(app: AppWithNotifications): boolean {
  switch (app.readiness) {
    case Readiness.kReady:
    case Readiness.kDisabledByBlocklist:
    case Readiness.kDisabledByPolicy:
    case Readiness.kDisabledByUser:
    case Readiness.kTerminated:
    case Readiness.kDisabledByLocalSettings:
      return true;
    case Readiness.kUninstalledByUser:
    case Readiness.kUninstalledByNonUser:
    case Readiness.kRemoved:
    case Readiness.kUnknown:
      return false;
  }
}

const OsSettingsAppsPageElementBase = DeepLinkingMixin(RouteOriginMixin(
    PrefsMixin(AppManagementStoreMixin(I18nMixin(PolymerElement)))));

export class OsSettingsAppsPageElement extends OsSettingsAppsPageElementBase {
  static get is() {
    return 'os-settings-apps-page' as const;
  }

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

  static get properties() {
    return {
      section_: {
        type: Number,
        value: Section.kApps,
        readOnly: true,
      },

      /**
       * This object holds the playStoreEnabled and settingsAppAvailable
       * boolean.
       */
      androidAppsInfo: Object,

      isPlayStoreAvailable_: {
        type: Boolean,
        value: () => {
          return isPlayStoreAvailable();
        },
      },

      searchTerm: String,

      showAndroidApps_: {
        type: Boolean,
        value: () => {
          return androidAppsVisible();
        },
      },

      isArcVmManageUsbAvailable_: {
        type: Boolean,
        value: () => {
          return isArcVmEnabled();
        },
      },

      /**
       * Whether the App Notifications page should be shown.
       */
      showAppNotificationsRow_: {
        type: Boolean,
        value() {
          return loadTimeData.getBoolean('showOsSettingsAppNotificationsRow');
        },
      },

      /**
       * Whether the Manage Isolated Web Apps page should be shown.
       */
      showManageIsolatedWebAppsRow_: {
        type: Boolean,
        value() {
          return loadTimeData.getBoolean('showManageIsolatedWebAppsRow');
        },
      },

      /**
       * Whether the Disable Parental Controls PIN dialog should be shown.
       */
      showParentalControlsDisablePinDialog_: {
        type: Boolean,
        value: false,
      },

      /**
       * Whether the Parental Controls PIN setup dialog should be shown.
       */
      showParentalControlsSetupPinDialog_: {
        type: Boolean,
        value: false,
      },

      /**
       * Whether the Parental Controls PIN verification dialog should be shown.
       */
      showParentalControlsVerifyPinDialog_: {
        type: Boolean,
        value: false,
      },

      /** Whether the user has set up app parental controls. */
      isParentalControlsSetupCompleted_: {
        type: Boolean,
        value: false,
      },

      isPluginVmAvailable_: {
        type: Boolean,
        value: () => {
          return isPluginVmAvailable();
        },
      },

      isAppParentalControlsFeatureAvailable_: {
        type: Boolean,
        value: () => {
          return isAppParentalControlsFeatureAvailable();
        },
        readOnly: true,
      },

      /**
       * Show On startup settings and sub-page.
       */
      shouldShowStartup_: {
        type: Boolean,
        value: () => {
          return shouldShowStartup();
        },
        readOnly: true,
      },

      app_: Object,

      appsWithNotifications_: {
        type: Array,
        value: [],
      },

      /**
       * List of options for the on startup drop-down menu.
       */
      onStartupOptions_: {
        readOnly: true,
        type: Array,
        value() {
          return [
            {value: 1, name: loadTimeData.getString('onStartupAlways')},
            {value: 2, name: loadTimeData.getString('onStartupAskEveryTime')},
            {value: 3, name: loadTimeData.getString('onStartupDoNotRestore')},
          ];
        },
      },

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

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

      /**
       * Used by DeepLinkingMixin to focus this page's deep links.
       */
      supportedSettingIds: {
        type: Object,
        value: () => new Set<Setting>([
          Setting.kManageAndroidPreferences,
          Setting.kTurnOnPlayStore,
          Setting.kRestoreAppsAndPages,
          Setting.kAppParentalControls,
        ]),
      },

      isRevampWayfindingEnabled_: {
        type: Boolean,
        value() {
          return isRevampWayfindingEnabled();
        },
        readOnly: true,
      },

      rowIcons_: {
        type: Object,
        value() {
          if (isRevampWayfindingEnabled()) {
            return {
              manageApps: 'os-settings:apps',
              notifications: 'os-settings:apps-notifications',
              googlePlayPreferences: 'os-settings:google-play-revamp',
              androidSettings: 'os-settings:apps-android-settings',
              manageIsolatedWebApps:
                  'os-settings:apps-manage-isolated-web-apps',
              parentalControls: 'os-settings:apps-parental-controls',
            };
          }

          return {
            manageApps: '',
            notifications: '',
            googlePlayPreferences: '',
            androidSettings: '',
            manageIsolatedWebApps: '',
            parentalControls: '',
          };
        },
      },
    };
  }

  androidAppsInfo: AndroidAppsInfo;
  searchTerm: string;
  private app_: App;
  private appNotificationsObserverReceiver_: AppNotificationsObserverReceiver;
  private appsWithNotifications_: AppWithNotifications[];
  private isArcVmManageUsbAvailable_: boolean;
  private isDndEnabled_: boolean;
  private isPinVerified_: boolean;
  private readonly isPlayStoreAvailable_: boolean;
  private isPluginVmAvailable_: boolean;
  private isRevampWayfindingEnabled_: boolean;
  private mojoInterfaceProvider_: AppNotificationsHandlerInterface;
  private parentalControlsHandler_: AppParentalControlsHandlerInterface;
  private onStartupOptions_: DropdownMenuOptionList;
  private rowIcons_: Record<string, string>;
  private section_: Section;
  private readonly showAndroidApps_: boolean;
  private showAppNotificationsRow_: boolean;
  private showManageIsolatedWebAppsRow_: boolean;
  private showParentalControlsDisablePinDialog_: boolean;
  private showParentalControlsSetupPinDialog_: boolean;
  private showParentalControlsVerifyPinDialog_: boolean;
  private isParentalControlsSetupCompleted_: boolean;
  private readonly shouldShowStartup_: boolean;

  constructor() {
    super();

    /** RouteOriginMixin override */
    this.route = routes.APPS;
  }

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

    this.watch('app_', state => {
      // Don't set `app_` to `null`, since it triggers Polymer
      // data bindings of <app-management-uninstall-button> which does not
      // accept `null`, use `undefined` instead.
      return getSelectedApp(state) || undefined;
    });

    this.mojoInterfaceProvider_ = getAppNotificationProvider();

    this.appNotificationsObserverReceiver_ =
        new AppNotificationsObserverReceiver(this);

    this.mojoInterfaceProvider_.addObserver(
        this.appNotificationsObserverReceiver_.$.bindNewPipeAndPassRemote());

    this.mojoInterfaceProvider_.getQuietMode().then((result) => {
      this.isDndEnabled_ = result.enabled;
    });
    this.mojoInterfaceProvider_.getApps().then((result) => {
      this.appsWithNotifications_ = result.apps;
    });

    this.parentalControlsHandler_ = getAppParentalControlsProvider();
    this.getIsParentalControlsSetupCompleted_().then((isCompleted) => {
      this.isParentalControlsSetupCompleted_ = isCompleted;
    });
  }

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

    this.addFocusConfig(routes.APP_MANAGEMENT, '#appManagementRow');
    this.addFocusConfig(routes.APP_NOTIFICATIONS, '#appNotificationsRow');
    this.addFocusConfig(
        routes.MANAGE_ISOLATED_WEB_APPS, '#manageIsolatedWebAppsRow');
    this.addFocusConfig(
        routes.ANDROID_APPS_DETAILS,
        () => this.shadowRoot!.querySelector<HTMLElement>(
            this.androidAppsInfo.playStoreEnabled ?
                '#androidApps .subpage-arrow' :
                '#arcEnable'));
  }

  override currentRouteChanged(newRoute: Route, oldRoute?: Route): void {
    super.currentRouteChanged(newRoute, oldRoute);

    // Does not apply to this page.
    if (newRoute !== this.route) {
      return;
    }

    this.attemptDeepLink();
  }

  private iconUrlFromId_(app: App): string {
    if (!app) {
      return '';
    }
    return getAppIcon(app);
  }

  private onClickAppManagement_(): void {
    chrome.metricsPrivate.recordEnumerationValue(
        AppManagementEntryPointsHistogramName,
        AppManagementEntryPoint.OS_SETTINGS_MAIN_PAGE,
        Object.keys(AppManagementEntryPoint).length);
    Router.getInstance().navigateTo(routes.APP_MANAGEMENT);
  }

  private onClickAppNotifications_(): void {
    Router.getInstance().navigateTo(routes.APP_NOTIFICATIONS);
  }

  private async getIsParentalControlsSetupCompleted_(): Promise<boolean> {
    const response = await this.parentalControlsHandler_.isSetupCompleted();
    return response.isCompleted;
  }

  private onClickParentalControls_(): void {
    this.getIsParentalControlsSetupCompleted_().then((isSetupCompleted) => {
      if (isSetupCompleted) {
        this.showParentalControlsVerifyPinDialog_ = true;
        recordParentalControlsDialogOpened(
            ParentalControlsDialogType.ENTER_SUBPAGE_VERIFICATION);
      }
    });
  }

  private setUpParentalControls_(e: Event): void {
    this.showParentalControlsSetupPinDialog_ = true;
    recordParentalControlsDialogOpened(
        ParentalControlsDialogType.SET_UP_CONTROLS);
    // Stop propagation to keep the subpage from opening.
    e.stopPropagation();
  }

  private disableParentalControls_(e: Event): void {
    this.showParentalControlsDisablePinDialog_ = true;
    recordParentalControlsDialogOpened(
        ParentalControlsDialogType.DISABLE_CONTROLS_VERIFICATION);
    // Stop propagation to keep the subpage from opening.
    e.stopPropagation();
  }

  private onAccessPinVerified_(): void {
    this.navigateToParentalControls_();
    recordParentalControlsDialogFlowCompleted(
        ParentalControlsDialogType.ENTER_SUBPAGE_VERIFICATION);
  }

  private onSetupPinSuccess_(): void {
    this.navigateToParentalControls_();

    this.getIsParentalControlsSetupCompleted_().then((isCompleted) => {
      this.isParentalControlsSetupCompleted_ = isCompleted;
    });
    recordParentalControlsDialogFlowCompleted(
        ParentalControlsDialogType.SET_UP_CONTROLS);
  }

  private onDisablePinVerified_(): void {
    this.parentalControlsHandler_.onControlsDisabled();
    this.getIsParentalControlsSetupCompleted_().then((isCompleted) => {
      this.isParentalControlsSetupCompleted_ = isCompleted;
    });
    recordParentalControlsDialogFlowCompleted(
        ParentalControlsDialogType.DISABLE_CONTROLS_VERIFICATION);
  }

  private onVerifyPinDialogClose_(): void {
    this.showParentalControlsVerifyPinDialog_ = false;
  }

  private onSetupPinDialogClose_(): void {
    this.showParentalControlsSetupPinDialog_ = false;
  }

  private async onDisablePinDialogClose_(): Promise<void> {
    this.showParentalControlsDisablePinDialog_ = false;
    const toggle =
        this.shadowRoot!.querySelector<HTMLElement>('#appParentalControls')!
            .querySelector<CrToggleElement>('#toggle');
    // If the toggle is still on the page, reset toggle in case the disable flow
    // was cancelled prior to completion.
    if (toggle) {
      toggle.checked = await this.getIsParentalControlsSetupCompleted_();
    }
  }

  private onClickManageIsolatedWebApps_(): void {
    Router.getInstance().navigateTo(routes.MANAGE_ISOLATED_WEB_APPS);
  }

  private onEnableAndroidAppsClick_(event: Event): void {
    this.setPrefValue('arc.enabled', true);
    event.stopPropagation();
  }

  private isEnforced_(pref: chrome.settingsPrivate.PrefObject): boolean {
    return pref.enforcement === chrome.settingsPrivate.Enforcement.ENFORCED;
  }

  private onAndroidAppsSubpageClick_(): void {
    if (this.androidAppsInfo.playStoreEnabled) {
      Router.getInstance().navigateTo(routes.ANDROID_APPS_DETAILS);
    }
  }

  private onManageAndroidAppsClick_(event: MouseEvent): void {
    // |event.detail| is the click count. Keyboard events will have 0 clicks.
    const isKeyboardAction = event.detail === 0;
    AndroidAppsBrowserProxyImpl.getInstance().showAndroidAppsSettings(
        isKeyboardAction);
  }

  /** Override ash.settings.appNotification.onNotificationAppChanged */
  onNotificationAppChanged(updatedApp: AppWithNotifications): void {
    const foundIdx = this.appsWithNotifications_.findIndex(app => {
      return app.id === updatedApp.id;
    });
    if (isAppInstalled(updatedApp)) {
      if (foundIdx !== -1) {
        this.splice('appsWithNotifications_', foundIdx, 1, updatedApp);
        return;
      }
      this.push('appsWithNotifications_', updatedApp);
      return;
    }

    // Cannot have an app that is uninstalled prior to being installed.
    assert(foundIdx !== -1);
    // Uninstalled app found, remove it from the list.
    this.splice('appsWithNotifications_', foundIdx, 1);
  }

  /** Override ash.settings.appNotification.onQuietModeChanged */
  onQuietModeChanged(enabled: boolean): void {
    this.isDndEnabled_ = enabled;
  }

  private getAppNotificationsRowSublabel_(): string {
    if (this.isRevampWayfindingEnabled_) {
      return this.i18n('appNotificationsRowSublabel');
    }

    return this.isDndEnabled_ ?
        this.i18n('appNotificationsDoNotDisturbEnabledDescription') :
        this.i18n(
            'appNotificationsCountDescription',
            this.appsWithNotifications_.length);
  }

  private navigateToParentalControls_(): void {
    this.isPinVerified_ = true;
    Router.getInstance().navigateTo(routes.APP_PARENTAL_CONTROLS);
  }
}

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

customElements.define(OsSettingsAppsPageElement.is, OsSettingsAppsPageElement);