[go: nahoru, domu]

blob: 67975b537363e9d7de443a454eb8083eabcf3d77 [file] [log] [blame]
// Copyright 2018 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
* value prop screen.
*
* Event 'loading' will be fired when the page is loading/reloading.
* Event 'error' will be fired when the webview failed to load.
* Event 'loaded' will be fired when the page has been successfully loaded.
*/
import '//resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import '//resources/polymer/v3_0/iron-icon/iron-icon.js';
import '../components/dialogs/oobe_adaptive_dialog.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 './assistant_common_styles.css.js';
import './assistant_icons.html.js';
import './setting_zippy.js';
import {loadTimeData} from '//resources/ash/common/load_time_data.m.js';
import {afterNextRender, html, mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {OobeDialogHostBehavior} from '../components/behaviors/oobe_dialog_host_behavior.js';
import {OobeI18nBehavior} from '../components/behaviors/oobe_i18n_behavior.js';
import {BrowserProxy, BrowserProxyImpl} from './browser_proxy.js';
import {AssistantNativeIconType, HtmlSanitizer, webviewStripLinksContentScript} from './utils.js';
/**
* Name of the screen.
* @type {string}
*/
const VALUE_PROP_SCREEN_ID = 'ValuePropScreen';
/**
* @constructor
* @extends {PolymerElement}
*/
const AssistantValuePropBase =
mixinBehaviors([OobeI18nBehavior, OobeDialogHostBehavior], PolymerElement);
/**
* @polymer
*/
class AssistantValueProp extends AssistantValuePropBase {
static get is() {
return `assistant-value-prop`;
}
static get template() {
return html`{__html_template__}`;
}
static get properties() {
return {
/**
* Buttons are disabled when the webview content is loading.
*/
buttonsDisabled: {
type: Boolean,
value: true,
},
/**
* The value prop URL template - loaded from loadTimeData.
* The template is expected to have '$' instead of the locale.
* @private {string}
*/
urlTemplate_: {
value:
'https://www.gstatic.com/opa-android/oobe/a02187e41eed9e42/v5_omni_$.html',
},
/**
* Default url for locale en_us.
*/
defaultUrl: {
type: String,
value: function() {
return this.urlTemplate_.replace('$', 'en_us');
},
},
/**
* Indicates whether user is minor mode user (e.g. under age of 18).
*/
isMinorMode_: {
type: Boolean,
value: false,
},
/**
* Indicates whether to use same design for accept/decline buttons.
*/
equalWeightButtons_: {
type: Boolean,
value: false,
},
/**
* Used to determine which activity control settings should be shown.
*/
currentConsentStep_: {
type: Number,
value: 0,
},
};
}
constructor() {
super();
/**
* 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 value prop webview object.
* @type {Object}
* @private
*/
this.valuePropView_ = null;
/**
* Whether the screen has been initialized.
* @type {boolean}
* @private
*/
this.initialized_ = false;
/**
* Whether the response header has been received for the value prop view.
* @type {boolean}
* @private
*/
this.headerReceived_ = false;
/**
* Whether the webview has been successfully loaded.
* @type {boolean}
* @private
*/
this.webViewLoaded_ = false;
/**
* Whether all the setting zippy has been successfully loaded.
* @type {boolean}
* @private
*/
this.settingZippyLoaded_ = 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;
/**
* Sanitizer used to sanitize html snippets.
* @type {HtmlSanitizer}
* @private
*/
this.sanitizer_ = new HtmlSanitizer();
/** @private {?BrowserProxy} */
this.browserProxy_ = BrowserProxyImpl.getInstance();
}
setUrlTemplateForTesting(url) {
this.urlTemplate_ = url;
}
/**
* On-tap event handler for skip button.
*
* @private
*/
onSkipTap_() {
if (this.buttonsDisabled) {
return;
}
this.buttonsDisabled = true;
this.browserProxy_.userActed(VALUE_PROP_SCREEN_ID, ['skip-pressed']);
}
/**
* On-tap event handler for next button.
*
* @private
*/
onNextTap_() {
if (this.buttonsDisabled) {
return;
}
this.buttonsDisabled = true;
this.browserProxy_.userActed(VALUE_PROP_SCREEN_ID, ['next-pressed']);
}
/**
* Sets learn more content text and shows it as overlay dialog.
* @param {string} title Title of the dialog.
* @param {string} content HTML formatted text to show.
* @param {string} buttonText Text on the button that closes the dialog.
*/
showLearnMoreOverlay(title, content, buttonText) {
this.$['overlay-title-text'].innerHTML =
this.sanitizer_.sanitizeHtml(title);
this.$['overlay-additional-info-text'].innerHTML =
this.sanitizer_.sanitizeHtml(content);
this.$['overlay-close-button-text'].textContent = buttonText;
this.$['overlay-close-button'].labelForAria = buttonText;
this.$['learn-more-overlay'].setTitleAriaLabel(title);
this.$['learn-more-overlay'].showModal();
this.$['overlay-close-button'].focus();
}
/**
* Hides overlay dialog.
*/
hideOverlay() {
this.$['learn-more-overlay'].close();
if (this.lastFocusedElement) {
this.lastFocusedElement.focus();
this.lastFocusedElement = null;
}
}
/**
* Reloads value prop page by fetching setting zippy and consent string.
*/
reloadPage() {
this.dispatchEvent(
new CustomEvent('loading', {bubbles: true, composed: true}));
if (this.initialized_) {
this.browserProxy_.userActed(VALUE_PROP_SCREEN_ID, ['reload-requested']);
this.settingZippyLoaded_ = false;
this.consentStringLoaded_ = false;
}
this.reloadWebView();
this.buttonsDisabled = true;
this.currentConsentStep_ = 0;
}
/**
* Reloads value prop animation webview.
*/
reloadWebView() {
this.loadingError_ = false;
this.headerReceived_ = false;
this.webViewLoaded_ = false;
const locale = loadTimeData.getString('assistantLocale')
.replace('-', '_')
.toLowerCase();
this.valuePropView_.src = this.urlTemplate_.replace('$', locale);
}
/**
* Handles event when value prop 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 value prop webview is loaded.
*/
onWebViewContentLoad(details) {
if (details == null) {
return;
}
if (this.loadingError_ || !this.headerReceived_) {
return;
}
if (this.reloadWithDefaultUrl_) {
this.valuePropView_.src = this.defaultUrl;
this.headerReceived_ = false;
this.reloadWithDefaultUrl_ = false;
return;
}
this.webViewLoaded_ = true;
if (this.settingZippyLoaded_ && this.consentStringLoaded_) {
this.onPageLoaded();
}
}
/**
* Handles event when webview request headers received.
*/
onWebViewHeadersReceived(details) {
if (details == null) {
return;
}
this.headerReceived_ = true;
if (details.statusCode === 404) {
if (details.url !== this.defaultUrl) {
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.$['value-prop-dialog'].setAttribute(
'aria-label', data['valuePropTitle']);
this.$['title-text'].textContent = data['valuePropTitle'];
this.$['next-button'].labelForAria = data['valuePropNextButton'];
this.$['next-button-text'].textContent = data['valuePropNextButton'];
this.$['skip-button'].labelForAria = data['valuePropSkipButton'];
this.$['skip-button-text'].textContent = data['valuePropSkipButton'];
this.$['footer-text'].innerHTML =
this.sanitizer_.sanitizeHtml(data['valuePropFooter']);
this.equalWeightButtons_ = data['equalWeightButtons'];
this.consentStringLoaded_ = true;
if (this.settingZippyLoaded_ && this.webViewLoaded_) {
this.onPageLoaded();
}
}
/**
* Add subtitles and setting zippys with given data.
*/
addSettingZippy(zippy_data) {
if (this.settingZippyLoaded_) {
if (this.consentStringLoaded_ && this.webViewLoaded_) {
this.onPageLoaded();
}
return;
}
// Clear containers to prevent contents being added multiple times.
while (this.$['subtitle-container'].firstElementChild) {
this.$['subtitle-container'].firstElementChild.remove();
}
while (this.$['consents-container'].firstElementChild) {
this.$['consents-container'].firstElementChild.remove();
}
// `zippy_data` contains a list of lists, where each list contains the
// setting zippys that should be shown on the same screen.
// `isMinorMode` is the same for all data in `zippy_data`. We could use the
// first one and set `isMinorMode_` flag.
this.isMinorMode_ = zippy_data[0][0]['isMinorMode'];
for (const i in zippy_data) {
this.addSubtitle_(zippy_data[i][0], i);
for (const j in zippy_data[i]) {
const data = zippy_data[i][j];
const zippy = document.createElement('setting-zippy');
if (data['useNativeIcons']) {
zippy.nativeIconType = data['nativeIconType'];
zippy.setAttribute('nativeIconLabel', data['title']);
} else {
// TODO(crbug.com/1313994) - Remove hard coded colors in OOBE
const background = this.isMinorMode_ ?
getComputedStyle(document.body)
.getPropertyValue('--cros-highlight-color' /* gblue50 */) :
getComputedStyle(document.body)
.getPropertyValue('--cros-bg-color');
zippy.setAttribute(
'icon-src',
'data:text/html;charset=utf-8,' +
encodeURIComponent(zippy.getWrappedIcon(
data['iconUri'], data['title'], background)));
}
zippy.setAttribute('step', i);
zippy.hideLine = this.isMinorMode_;
zippy.cardStyle = this.isMinorMode_;
const title = document.createElement('div');
title.slot = 'title';
title.innerHTML = this.sanitizer_.sanitizeHtml(data['name']);
zippy.appendChild(title);
const content = document.createElement('div');
content.slot = 'content';
const description = document.createElement('div');
description.innerHTML =
this.sanitizer_.sanitizeHtml(data['description'] + ' ');
const learnMoreLink = document.createElement('a');
learnMoreLink.textContent = data['popupLink'];
learnMoreLink.setAttribute('href', 'javascript:void(0)');
learnMoreLink.onclick =
function(title, content, buttonText, focus) {
this.lastFocusedElement = focus;
this.showLearnMoreOverlay(title, content, buttonText);
}.bind(this, data['learnMoreDialogTitle'],
data['learnMoreDialogContent'], data['learnMoreDialogButton'],
learnMoreLink);
description.appendChild(learnMoreLink);
content.appendChild(description);
if (this.isMinorMode_) {
const additionalInfo = document.createElement('div');
additionalInfo.innerHTML =
this.sanitizer_.sanitizeHtml(data['additionalInfo']);
content.appendChild(document.createElement('br'));
content.appendChild(additionalInfo);
}
zippy.appendChild(content);
this.$['consents-container'].appendChild(zippy);
}
}
this.showContentForStep_(this.currentConsentStep_);
this.settingZippyLoaded_ = true;
if (this.consentStringLoaded_ && this.webViewLoaded_) {
this.onPageLoaded();
}
}
/**
* Add a subtitle for step with given data.
*/
addSubtitle_(data, step) {
const subtitle = document.createElement('div');
subtitle.setAttribute('step', step);
if (this.isMinorMode_) {
const title = document.createElement('div');
title.innerHTML = this.sanitizer_.sanitizeHtml(data['title']);
title.classList.add('subtitle-text');
subtitle.appendChild(title);
const username = document.createElement('div');
username.innerHTML = this.sanitizer_.sanitizeHtml(data['identity']);
username.classList.add('username-text');
subtitle.appendChild(username);
}
const message = document.createElement('div');
message.innerHTML = this.sanitizer_.sanitizeHtml(data['intro']);
message.classList.add(
this.isMinorMode_ ? 'subtitle-message-text-minor' :
'subtitle-message-text');
subtitle.appendChild(message);
this.$['subtitle-container'].appendChild(subtitle);
}
/**
* Handles event when all the page content has been loaded.
*/
onPageLoaded() {
this.dispatchEvent(
new CustomEvent('loaded', {bubbles: true, composed: true}));
// The webview animation only starts playing when it is focused (in order
// to make sure the animation and the caption are in sync).
this.valuePropView_.focus();
this.async(() => {
this.buttonsDisabled = false;
this.$['next-button'].focus();
}, 300);
if (!this.hidden && !this.screenShown_) {
this.browserProxy_.screenShown(VALUE_PROP_SCREEN_ID);
this.screenShown_ = true;
}
}
/**
* Signal from host to show the screen.
*/
onShow() {
this.$['overlay-close-button'].addEventListener(
'click', () => this.hideOverlay());
afterNextRender(this, () => this.$['next-button'].focus());
if (!this.initialized_) {
this.valuePropView_ = this.$['value-prop-view'];
this.initializeWebview_(this.valuePropView_);
this.reloadPage();
this.initialized_ = true;
}
}
/**
* Update the screen to show the next settings with updated subtitle and
* setting zippy. This is called only for minor users as settings are
* unbundled.
*/
showNextStep() {
this.currentConsentStep_ += 1;
this.showContentForStep_(this.currentConsentStep_);
this.buttonsDisabled = false;
this.$['next-button'].focus();
}
/**
* Update visibility of subtitles and setting zippys for a given step.
* @param {number} step
*/
showContentForStep_(step) {
for (const subtitle of this.$['subtitle-container'].children) {
subtitle.hidden = parseInt(subtitle.getAttribute('step')) !== step;
}
for (const zippy of this.$['consents-container'].children) {
zippy.hidden = parseInt(zippy.getAttribute('step')) !== step;
}
}
initializeWebview_(webview) {
const requestFilter = {urls: ['<all_urls>'], types: ['main_frame']};
webview.request.onErrorOccurred.addListener(
details => this.onWebViewErrorOccurred(details), requestFilter);
webview.request.onHeadersReceived.addListener(
details => this.onWebViewHeadersReceived(details), requestFilter);
webview.addEventListener(
'contentload', details => this.onWebViewContentLoad(details));
webview.addContentScripts([webviewStripLinksContentScript]);
}
/**
* Returns the webview animation container.
*/
getAnimationContainer() {
return this.$['animation-container'];
}
}
customElements.define(AssistantValueProp.is, AssistantValueProp);