chromium/chrome/browser/resources/chromeos/assistant_optin/assistant_related_info.js

// 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.

/**
 * @fileoverview Polymer element for displaying material design assistant
 * related info screen.
 *
 * Event 'loading' will be fired when the page is loading/reloading.
 * Event 'loaded' will be fired when the page has been successfully loaded.
 */

import '//resources/ash/common/cr_elements/cr_lottie/cr_lottie.js';
import '//resources/polymer/v3_0/iron-icon/iron-icon.js';
import '../components/buttons/oobe_next_button.js';
import '../components/buttons/oobe_text_button.js';
import '../components/common_styles/oobe_dialog_host_styles.css.js';
import '../components/dialogs/oobe_adaptive_dialog.js';
import './assistant_common_styles.css.js';
import './assistant_icons.html.js';
import './setting_zippy.js';

import {afterNextRender, html, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {OobeDialogHostMixin} from '../components/mixins/oobe_dialog_host_mixin.js';
import {OobeI18nMixin} from '../components/mixins/oobe_i18n_mixin.js';

import {BrowserProxyImpl} from './browser_proxy.js';
import {AssistantNativeIconType, webviewStripLinksContentScript} from './utils.js';


/**
 * Name of the screen.
 * @type {string}
 */
const RELATED_INFO_SCREEN_ID = 'RelatedInfoScreen';

/**
 * @constructor
 * @extends {PolymerElement}
 */
const AssistantRelatedInfoBase =
    OobeDialogHostMixin(OobeI18nMixin(PolymerElement));

/**
 * @polymer
 */
class AssistantRelatedInfo extends AssistantRelatedInfoBase {
  static get is() {
    return 'assistant-related-info';
  }

  static get template() {
    return html`{__html_template__}`;
  }

  static get properties() {
    return {
      /**
       * Whether page content is loading.
       */
      loading: {
        type: Boolean,
        value: true,
      },

      /**
       * Whether activity control consent is skipped.
       */
      skipActivityControl_: {
        type: Boolean,
        value: false,
      },

      /**
       * Indicates whether to use same design for accept/decline buttons.
       */
      equalWeightButtons_: {
        type: Boolean,
        value: false,
      },

      /**
       * The given name of the user, if a child account is in use; otherwise,
       * this is an empty string.
       */
      childName_: {
        type: String,
        value: '',
      },

      /**
       * Whether the marketing opt-in page is being rendered in dark mode.
       * @private {boolean}
       */
      isDarkModeActive_: {
        type: Boolean,
        value: false,
      },
    };
  }

  constructor() {
    super();

    /**
     * The animation URL template - loaded from loadTimeData.
     * The template is expected to have '$' instead of the locale.
     * @private {string}
     */
    this.urlTemplate_ =
        'https://www.gstatic.com/opa-android/oobe/a02187e41eed9e42/v5_omni_$.html';

    /**
     * Whether try to reload with the default url when a 404 error occurred.
     * @type {boolean}
     * @private
     */
    this.reloadWithDefaultUrl_ = false;

    /**
     * Whether an error occurs while the webview is loading.
     * @type {boolean}
     * @private
     */
    this.loadingError_ = false;

    /**
     * The animation webview object.
     * @type {Object}
     * @private
     */
    this.webview_ = null;

    /**
     * Whether the screen has been initialized.
     * @type {boolean}
     * @private
     */
    this.initialized_ = false;

    /**
     * Whether the response header has been received for the animation webview.
     * @type {boolean}
     * @private
     */
    this.headerReceived_ = false;

    /**
     * Whether the webview has been successfully loaded.
     * @type {boolean}
     * @private
     */
    this.webViewLoaded_ = false;

    /**
     * Whether all the consent text strings has been successfully loaded.
     * @type {boolean}
     * @private
     */
    this.consentStringLoaded_ = false;

    /**
     * Whether the screen has been shown to the user.
     * @type {boolean}
     * @private
     */
    this.screenShown_ = false;

    /** @private {?BrowserProxy} */
    this.browserProxy_ = BrowserProxyImpl.getInstance();
  }

  setUrlTemplateForTesting(url) {
    this.urlTemplate_ = url;
  }

  /**
   * On-tap event handler for next button.
   *
   * @private
   */
  onNextTap_() {
    if (this.loading) {
      return;
    }
    this.loading = true;
    this.browserProxy_.userActed(RELATED_INFO_SCREEN_ID, ['next-pressed']);
  }

  /**
   * On-tap event handler for skip button.
   *
   * @private
   */
  onSkipTap_() {
    if (this.loading) {
      return;
    }
    this.loading = true;
    this.browserProxy_.userActed(RELATED_INFO_SCREEN_ID, ['skip-pressed']);
  }

  /**
   * Reloads the page.
   */
  reloadPage() {
    this.dispatchEvent(
        new CustomEvent('loading', {bubbles: true, composed: true}));
    this.loading = true;
    this.loadingError_ = false;
    this.headerReceived_ = false;
    const locale = this.locale.replace('-', '_').toLowerCase();
    this.webview_.src = this.urlTemplate_.replace('$', locale);
  }

  /**
   * Handles event when animation webview cannot be loaded.
   */
  onWebViewErrorOccurred(details) {
    if (details && details.error === 'net::ERR_ABORTED') {
      // Retry triggers net::ERR_ABORTED, so ignore it.
      // TODO(b/232592745): Replace with a state machine to handle aborts
      // gracefully and avoid duplicate reloads.
      return;
    }
    this.dispatchEvent(
        new CustomEvent('error', {bubbles: true, composed: true}));
    this.loadingError_ = true;
  }

  /**
   * Handles event when animation webview is loaded.
   */
  onWebViewContentLoad(details) {
    if (details == null) {
      return;
    }
    if (this.loadingError_ || !this.headerReceived_) {
      return;
    }
    if (this.reloadWithDefaultUrl_) {
      this.webview_.src = this.getDefaultAnimationUrl_();
      this.headerReceived_ = false;
      this.reloadWithDefaultUrl_ = false;
      return;
    }

    this.webViewLoaded_ = true;
    if (this.consentStringLoaded_) {
      this.onPageLoaded();
    }

    // The webview animation only starts playing when it is focused (in order
    // to make sure the animation and the caption are in sync).
    this.webview_.focus();
    setTimeout(() => {
      if (!this.equalWeightButtons_) {
        this.$['next-button'].focus();
      }
    }, 300);
  }

  /**
   * Handles event when webview request headers received.
   */
  onWebViewHeadersReceived(details) {
    if (details == null) {
      return;
    }
    this.headerReceived_ = true;
    if (details.statusCode === 404) {
      if (details.url !== this.getDefaultAnimationUrl_()) {
        this.reloadWithDefaultUrl_ = true;
        return;
      } else {
        this.onWebViewErrorOccurred();
      }
    } else if (details.statusCode !== 200) {
      this.onWebViewErrorOccurred();
    }
  }

  /**
   * Reload the page with the given consent string text data.
   */
  reloadContent(data) {
    this.skipActivityControl_ = !data['activityControlNeeded'];
    this.childName_ = data['childName'];
    if (!data['useNativeIcons']) {
      const url = this.isDarkModeActive_ ? 'info_outline_gm_grey500_24dp.png' :
                                           'info_outline_gm_grey600_24dp.png';
      this.$.zippy.setAttribute(
          'icon-src',
          'data:text/html;charset=utf-8,' +
              encodeURIComponent(this.$.zippy.getWrappedIcon(
                  'https://www.gstatic.com/images/icons/material/system/2x/' +
                      url,
                  this.i18n('assistantScreenContextTitle'),
                  getComputedStyle(document.body)
                      .getPropertyValue('--cros-bg-color'))));
      this.$.zippy.nativeIconType = AssistantNativeIconType.NONE;
    } else {
      this.$.zippy.nativeIconType = AssistantNativeIconType.INFO;
    }
    this.equalWeightButtons_ = data['equalWeightButtons'];

    this.consentStringLoaded_ = true;
    if (this.webViewLoaded_) {
      this.onPageLoaded();
    }
  }

  /**
   * Handles event when all the page content has been loaded.
   */
  onPageLoaded() {
    this.dispatchEvent(
        new CustomEvent('loaded', {bubbles: true, composed: true}));
    this.loading = false;
    if (!this.equalWeightButtons_) {
      this.$['next-button'].focus();
    }
    if (!this.hidden && !this.screenShown_) {
      this.browserProxy_.screenShown(RELATED_INFO_SCREEN_ID);
      this.screenShown_ = true;
    }
  }

  /**
   * Signal from host to show the screen.
   */
  onShow() {
    if (!this.initialized_) {
      this.webview_ = this.$['assistant-animation-webview'];
      this.initializeWebview_(this.webview_);
      this.reloadPage();
      this.initialized_ = true;
    } else {
      afterNextRender(this, () => {
        if (!this.equalWeightButtons_) {
          this.$['next-button'].focus();
        }
      });
      this.browserProxy_.screenShown(RELATED_INFO_SCREEN_ID);
      this.screenShown_ = true;
    }
  }

  initializeWebview_(webview) {
    const requestFilter = {urls: ['<all_urls>'], types: ['main_frame']};
    webview.request.onErrorOccurred.addListener(
        this.onWebViewErrorOccurred.bind(this), requestFilter);
    webview.request.onHeadersReceived.addListener(
        this.onWebViewHeadersReceived.bind(this), requestFilter);
    webview.addEventListener(
        'contentload', this.onWebViewContentLoad.bind(this));
    webview.addContentScripts([webviewStripLinksContentScript]);
  }

  /**
   * Get default animation url for locale en.
   */
  getDefaultAnimationUrl_() {
    return this.urlTemplate_.replace('$', 'en_us');
  }

  /**
   * Returns the webview animation container.
   */
  getAnimationContainer() {
    return this.$['animation-container'];
  }

  /**
   * Returns the title of the dialog.
   */
  getDialogTitle_(locale, skipActivityControl, childName) {
    if (skipActivityControl) {
      return this.i18n('assistantRelatedInfoReturnedUserTitle');
    } else {
      return childName ?
          this.i18n('assistantRelatedInfoTitleForChild', childName) :
          this.i18n('assistantRelatedInfoTitle');
    }
  }
}

customElements.define(AssistantRelatedInfo.is, AssistantRelatedInfo);