blob: 44e3c44481598fb267bdea93917e78367ddc7c63 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Chrome window that hosts UI. Only one window is allowed.
* @type {chrome.app.window.AppWindow}
*/
var appWindow = null;
/** @type {TermsOfServicePage} */
var termsPage = null;
/** @type {ActiveDirectoryAuthPage} */
var activeDirectoryAuthPage = null;
/**
* Used for bidirectional communication with native code.
* @type {chrome.runtime.Port}
*/
var port = null;
/**
* Stores current device id.
* @type {string}
*/
var currentDeviceId = null;
/**
* Stores last focused element before showing overlay. It is used to restore
* focus once overlay is closed.
* @type {Object}
*/
var lastFocusedElement = null;
/**
* Stores locale set for the current browser process.
* @type {string}
*/
var locale = null;
/**
* Host window outer default width.
* @const {number}
*/
var OUTER_WIDTH = 768;
/**
* Host window outer default height.
* @const {number}
*/
var OUTER_HEIGHT = 640;
/**
* Contains list of possible combination for languages and country codes. If
* match is found then navigate to final document directly.
*/
var PLAYSTORE_TOS_LOCALIZATIONS = [
'id_id', 'bs_ba', 'ca_es', 'cs_cz', 'da_dk', 'de_be',
'de_de', 'de_li', 'de_lu', 'de_at', 'de_ch', 'et_ee',
'en_as', 'en_ag', 'en_au', 'en_bs', 'en_bh', 'en_bz',
'en_bw', 'en_kh', 'en_cm', 'en_ca', 'en_cy', 'en_eg',
'en_fj', 'en_gu', 'en_is', 'en_in', 'en_ie', 'en_il',
'en_it', 'en_jo', 'en_kw', 'en_lb', 'en_mh', 'en_mu',
'en_na', 'en_np', 'en_nz', 'en_mp', 'en_om', 'en_pw',
'en_pg', 'en_ph', 'en_qa', 'en_rw', 'en_sa', 'en_sg',
'en_za', 'en_lk', 'en_ch', 'en_tz', 'en_tt', 'en_vi',
'en_ug', 'en_ae', 'en_uk', 'en_us', 'en_zm', 'en_zw',
'es_es', 'es_us', 'es_gu', 'es_as', 'es-419_ar', 'es-419_bo',
'es-419_cl', 'es-419_co', 'es-419_cr', 'es-419_cu', 'es-419_ec', 'es-419_sv',
'es-419_us', 'es-419_gt', 'es-419_hn', 'es-419_mx', 'es-419_ni', 'es-419_pa',
'es-419_py', 'es-419_pe', 'es-419_pr', 'es-419_do', 'es-419_uy', 'es-419_ve',
'fr_be', 'fr_bj', 'fr_bf', 'fr_kh', 'fr_cm', 'fr_ca',
'fr_ci', 'fr_fr', 'fr_ga', 'fr_lu', 'fr_ml', 'fr_mu',
'fr_ne', 'fr_sn', 'fr_ch', 'fr_tg', 'hl_in', 'hr_hr',
'it_it', 'it_it', 'lv_lv', 'lt_lt', 'hu_hu', 'mt_mt',
'nl_aw', 'nl_be', 'nl_nl', 'no_no', 'pl_pl', 'pt-BR_br',
'pt-PT_ao', 'pt-PT_cv', 'pt-PT_gw', 'pt-PT_mz', 'pt-PT_pt', 'ro_md',
'ro_ro', 'sq_al', 'sk_sk', 'sl_si', 'fi_fi', 'sv_se',
'vi_vn', 'tr_cy', 'tr_tr', 'el_gr', 'el_cy', 'be_by',
'bg_bg', 'mk_mk', 'ru_az', 'ru_am', 'ru_by', 'ru_ba',
'ru_kz', 'ru_kg', 'ru_ru', 'ru_tj', 'ru_tm', 'ru_uz',
'sr_rs', 'uk_ua', 'hy_am', 'ar_jo', 'ar_ae', 'ar_bh',
'ar_kw', 'ar_sa', 'ar_om', 'ar_qa', 'ar_lb', 'ar_eg',
'hi_in', 'th_th', 'th_la', 'ko_kr', 'zh-CN_cn', 'zh-TW_tw',
'zh-TW_hk', 'ja_jp',
];
/**
* Sends a native message to ArcSupportHost.
* @param {string} event The event type in message.
* @param {Object=} opt_props Extra properties for the message.
*/
function sendNativeMessage(event, opt_props) {
var message = Object.assign({'event': event}, opt_props);
port.postMessage(message);
}
/**
* Class to handle checkbox corresponding to a preference.
*/
class PreferenceCheckbox {
/**
* Creates a Checkbox which handles the corresponding preference update.
* @param {Element} container The container this checkbox corresponds to.
* The element must have <input type="checkbox" class="checkbox-option">
* for the checkbox itself, and <p class="checkbox-text"> for its label.
* @param {string} learnMoreContent I18n content which is shown when "Learn
* More" link is clicked.
* @param {string?} learnMoreLinkId The ID for the "Learn More" link element.
* TODO: Get rid of this. The element can have class so that it can be
* identified easily. Also, it'd be better to extract the link element
* (tag) from the i18n text, and let i18n focus on the content.
* @param {string?} policyText The content of the policy indicator.
*/
constructor(container, learnMoreContent, learnMoreLinkId, policyText) {
this.container_ = container;
this.learnMoreContent_ = learnMoreContent;
this.checkbox_ = container.querySelector('.checkbox-option');
this.label_ = container.querySelector('.checkbox-text');
this.isManaged_ = false;
var learnMoreLink = this.label_.querySelector(learnMoreLinkId);
if (learnMoreLink) {
learnMoreLink.addEventListener(
'click', (event) => this.onLearnMoreLinkClicked(event));
learnMoreLink.addEventListener(
'keydown', (event) => this.suppressKeyDown(event));
}
// Create controlled indicator for policy if necessary.
if (policyText) {
this.policyIndicator_ =
new appWindow.contentWindow.cr.ui.ControlledIndicator();
this.policyIndicator_.setAttribute('textpolicy', policyText);
// TODO: better to have a dedicated element for this place.
this.label_.insertBefore(this.policyIndicator_, learnMoreLink);
} else {
this.policyIndicator_ = null;
}
}
/**
* Returns if the checkbox is checked or not. Note that this *may* be
* different from the preference value, because the user's check is
* not propagated to the preference until the user clicks "AGREE" button.
*/
isChecked() {
return this.checkbox_.checked;
}
/**
* Returns if the checkbox reflects a managed setting, rather than a
* user-controlled setting.
*/
isManaged() {
return this.isManaged_;
}
/**
* Called when the preference value in native code is updated.
*/
onPreferenceChanged(isEnabled, isManaged) {
this.checkbox_.checked = isEnabled;
this.checkbox_.disabled = isManaged;
this.label_.disabled = isManaged;
this.isManaged_ = isManaged;
if (this.policyIndicator_) {
if (isManaged) {
this.policyIndicator_.setAttribute('controlled-by', 'policy');
} else {
this.policyIndicator_.removeAttribute('controlled-by');
}
}
}
/**
* Called when the "Learn More" link is clicked.
*/
onLearnMoreLinkClicked(event) {
showTextOverlay(this.learnMoreContent_);
event.stopPropagation();
}
/**
* Called when a key is pressed down on the "Learn More" or "Settings" links.
* This prevent propagation of the current event in order to prevent parent
* check box toggles its state.
*/
suppressKeyDown(event) {
event.stopPropagation();
}
}
/**
* Handles the checkbox action of metrics preference.
* This has special customization e.g. show/hide the checkbox based on
* the native preference.
*/
class MetricsPreferenceCheckbox extends PreferenceCheckbox {
constructor(
container, learnMoreContent, learnMoreLinkId, isOwner, textDisabled,
textEnabled, textManagedDisabled, textManagedEnabled) {
// Do not use policy indicator.
// Learn More link handling is done by this class.
// So pass |null| intentionally.
super(container, learnMoreContent, null, null);
this.textLabel_ = container.querySelector('.content-text');
this.learnMoreLinkId_ = learnMoreLinkId;
this.isOwner_ = isOwner;
// Two dimensional array. First dimension is whether it is managed or not,
// the second one is whether it is enabled or not.
this.texts_ = [
[textDisabled, textEnabled],
[textManagedDisabled, textManagedEnabled],
];
}
onPreferenceChanged(isEnabled, isManaged) {
isManaged = isManaged || !this.isOwner_;
super.onPreferenceChanged(isEnabled, isManaged);
// Hide the checkbox if it is not allowed to (re-)enable.
// TODO(jhorwich) Remove checkbox functionality from the metrics notice as
// we've removed the ability for a device owner to enable it during ARC
// setup.
var canEnable = false;
this.checkbox_.hidden = !canEnable;
this.textLabel_.hidden = canEnable;
var label = canEnable ? this.label_ : this.textLabel_;
// Update label text.
label.innerHTML = this.texts_[isManaged ? 1 : 0][isEnabled ? 1 : 0];
// Work around for the current translation text.
// The translation text has tags for following links, although those
// tags are not the target of the translation (but those content text is
// the translation target).
// So, meanwhile, we set the link everytime we update the text.
// TODO: fix the translation text, and main html.
var learnMoreLink = label.querySelector(this.learnMoreLinkId_);
if (learnMoreLink) {
learnMoreLink.addEventListener(
'click', (event) => this.onLearnMoreLinkClicked(event));
learnMoreLink.addEventListener(
'keydown', (event) => this.suppressKeyDown(event));
}
// settings-link is used only in privacy section.
var settingsLink = label.querySelector('#settings-link');
if (settingsLink) {
settingsLink.addEventListener(
'click', (event) => this.onPrivacySettingsLinkClicked(event));
settingsLink.addEventListener(
'keydown', (event) => this.suppressKeyDown(event));
}
}
/** Called when "privacy settings" link is clicked. */
onPrivacySettingsLinkClicked(event) {
sendNativeMessage('onOpenPrivacySettingsPageClicked');
event.stopPropagation();
}
}
/**
* Represents the page loading state.
* @enum {number}
*/
var LoadState = {
UNLOADED: 0,
LOADING: 1,
ABORTED: 2,
LOADED: 3,
};
/**
* Handles events for Terms-Of-Service page. Also this implements the async
* loading of Terms-Of-Service content.
*/
class TermsOfServicePage {
/**
* @param {Element} container The container of the page.
* @param {boolean} isManaged Set true if ARC is managed.
* @param {string} countryCode The country code for the terms of service.
* @param {MetricsPreferenceCheckbox} metricsCheckbox. The checkbox for the
* metrics preference.
* @param {PreferenceCheckbox} backupRestoreCheckbox The checkbox for the
* backup-restore preference.
* @param {PreferenceCheckbox} locationServiceCheckbox The checkbox for the
* location service.
* @param {string} learnMorePaiService. Contents of learn more link of Play
* auto install service.
*/
constructor(
container, isManaged, countryCode, metricsCheckbox, backupRestoreCheckbox,
locationServiceCheckbox, learnMorePaiService) {
this.loadingContainer_ =
container.querySelector('#terms-of-service-loading');
this.contentContainer_ =
container.querySelector('#terms-of-service-content');
this.metricsCheckbox_ = metricsCheckbox;
this.backupRestoreCheckbox_ = backupRestoreCheckbox;
this.locationServiceCheckbox_ = locationServiceCheckbox;
this.isManaged_ = isManaged;
this.tosContent_ = '';
this.tosShown_ = false;
// Set event listener for webview loading.
this.termsView_ = container.querySelector('#terms-view');
this.termsView_.addEventListener(
'loadstart', () => this.onTermsViewLoadStarted_());
this.termsView_.addEventListener(
'contentload', () => this.onTermsViewLoaded_());
this.termsView_.addEventListener(
'loadabort', (event) => this.onTermsViewLoadAborted_(event.reason));
var requestFilter = {urls: ['<all_urls>'], types: ['main_frame']};
this.termsView_.request.onCompleted.addListener(
this.onTermsViewRequestCompleted_.bind(this), requestFilter);
this.countryCode = countryCode.toLowerCase();
var scriptInitTermsView =
'document.countryCode = \'' + this.countryCode + '\';';
scriptInitTermsView += 'document.language = \'' + locale + '\';';
scriptInitTermsView += 'document.viewMode = \'large-view\';';
this.termsView_.addContentScripts([
{
name: 'preProcess',
matches: ['https://play.google.com/*'],
js: {code: scriptInitTermsView},
run_at: 'document_start'
},
{
name: 'postProcess',
matches: ['https://play.google.com/*'],
css: {files: ['playstore.css']},
js: {files: ['playstore.js']},
run_at: 'document_end'
}
]);
// webview is not allowed to open links in the new window. Hook these
// events and open links in overlay dialog.
this.termsView_.addEventListener('newwindow', function(event) {
event.preventDefault();
showURLOverlay(event.targetUrl);
});
this.state_ = LoadState.UNLOADED;
this.serviceContainer_ = container.querySelector('#service-container');
this.locationService_ =
container.querySelector('#location-service-preference');
this.paiService_ = container.querySelector('#pai-service-descirption');
this.googleServiceConfirmation_ =
container.querySelector('#google-service-confirmation');
this.agreeButton_ = container.querySelector('#button-agree');
this.nextButton_ = container.querySelector('#button-next');
// On managed case, do not show TermsOfService section. Note that the
// checkbox for the prefereces are still visible.
var visibility = isManaged ? 'hidden' : 'visible';
container.querySelector('#terms-container').style.visibility = visibility;
// PAI service.
var paiLabel = this.paiService_.querySelector('.content-text');
var paiLearnMoreLink = paiLabel.querySelector('#learn-more-link-pai');
if (paiLearnMoreLink) {
paiLearnMoreLink.onclick = function(event) {
event.stopPropagation();
showTextOverlay(learnMorePaiService);
};
}
// Set event handler for buttons.
this.agreeButton_.addEventListener('click', () => this.onAgree());
this.nextButton_.addEventListener('click', () => this.onNext_());
container.querySelector('#button-cancel')
.addEventListener('click', () => this.onCancel_());
}
/** Called when the TermsOfService page is shown. */
onShow() {
if (this.isManaged_ || this.state_ == LoadState.LOADED) {
// Note: in managed case, because it does not show the contents of terms
// of service, it is ok to show the content container immediately.
this.showContent_();
} else {
this.startTermsViewLoading_();
}
}
/** Shows the loaded terms-of-service content. */
showContent_() {
this.loadingContainer_.hidden = true;
this.contentContainer_.hidden = false;
this.locationService_.hidden = true;
this.paiService_.hidden = true;
this.googleServiceConfirmation_.hidden = true;
this.serviceContainer_.style.overflow = 'hidden';
this.agreeButton_.hidden = true;
this.nextButton_.hidden = false;
this.updateTermsHeight_();
this.nextButton_.focus();
}
onNext_() {
this.locationService_.hidden = false;
this.paiService_.hidden = false;
this.googleServiceConfirmation_.hidden = false;
this.serviceContainer_.style.overflowY = 'auto';
this.serviceContainer_.scrollTop = this.serviceContainer_.scrollHeight;
this.agreeButton_.hidden = false;
this.nextButton_.hidden = true;
this.agreeButton_.focus();
}
/**
* Updates terms view height manually because webview is not automati
* cally
* resized in case parent div element gets resized.
*/
updateTermsHeight_() {
// Update the height in next cycle to prevent webview animation and
// wrong layout caused by whole-page layout change.
setTimeout(function() {
var doc = appWindow.contentWindow.document;
// Reset terms-view height in order to stabilize style computation. For
// some reason, child webview affects final result.
this.termsView_.style.height = '0px';
var termsContainer =
this.contentContainer_.querySelector('#terms-container');
var style = window.getComputedStyle(termsContainer, null);
this.termsView_.style.height = style.getPropertyValue('height');
}.bind(this), 0);
}
/** Starts to load the terms of service webview content. */
startTermsViewLoading_() {
if (this.state_ == LoadState.LOADING) {
// If there already is inflight loading task, do nothing.
return;
}
var defaultLocation = 'https://play.google.com/about/play-terms/';
if (this.termsView_.src) {
// This is reloading the page, typically clicked RETRY on error page.
this.fastLocation_ = undefined;
if (this.termsView_.src == defaultLocation) {
this.termsView_.reload();
} else {
this.termsView_.src = defaultLocation;
}
} else {
// Try fast load first if we know location.
this.fastLocation_ = this.getFastLocation_();
if (this.fastLocation_) {
this.termsView_.src = 'https://play.google.com/intl/' +
this.fastLocation_ + '/about/play-terms/';
} else {
this.termsView_.src = defaultLocation;
}
}
}
/**
* Checks the combination of the current language and country code and tries
* to resolve known terms location. This location is used to load terms
* directly in required language and zone. This prevents extra navigation to
* default terms page to determine this target location.
* Returns undefined in case the fast location cannot be found.
*/
getFastLocation_() {
var matchByLangZone = locale + '_' + this.countryCode;
if (PLAYSTORE_TOS_LOCALIZATIONS.indexOf(matchByLangZone) >= 0) {
return matchByLangZone;
}
var langSegments = locale.split('-');
if (langSegments.length == 2) {
var matchByShortLangZone = langSegments[0] + '_' + this.countryCode;
if (PLAYSTORE_TOS_LOCALIZATIONS.indexOf(matchByShortLangZone) >= 0) {
return matchByShortLangZone;
}
}
return undefined;
}
/** Returns user choices and page configuration for processing. */
getPageResults_() {
return {
tosContent: this.tosContent_,
tosShown: this.tosShown_,
isMetricsEnabled: this.metricsCheckbox_.isChecked(),
isBackupRestoreEnabled: this.backupRestoreCheckbox_.isChecked(),
isBackupRestoreManaged: this.backupRestoreCheckbox_.isManaged(),
isLocationServiceEnabled: this.locationServiceCheckbox_.isChecked(),
isLocationServiceManaged: this.locationServiceCheckbox_.isManaged()
};
}
/** Called when the terms-view starts to be loaded. */
onTermsViewLoadStarted_() {
// Note: Reloading can be triggered by user action. E.g., user may select
// their language by selection at the bottom of the Terms Of Service
// content.
this.state_ = LoadState.LOADING;
this.tosContent_ = '';
// Show loading page.
this.loadingContainer_.hidden = false;
this.contentContainer_.hidden = true;
}
/** Called when the terms-view is loaded. */
onTermsViewLoaded_() {
// This is called also when the loading is failed.
// In such a case, onTermsViewLoadAborted_() is called in advance, and
// state_ is set to ABORTED. Here, switch the view only for the
// successful loading case.
if (this.state_ == LoadState.LOADING) {
var getToSContent = {code: 'getToSContent();'};
termsPage.termsView_.executeScript(
getToSContent, this.onGetToSContent_.bind(this));
}
}
/** Callback for getToSContent. */
onGetToSContent_(results) {
if (this.state_ == LoadState.LOADING) {
if (!results || results.length != 1 || typeof results[0] !== 'string') {
this.onTermsViewLoadAborted_('unable to get ToS content');
return;
}
this.state_ = LoadState.LOADED;
this.tosContent_ = results[0];
this.tosShown_ = true;
this.showContent_();
if (this.fastLocation_) {
// For fast location load make sure we have right terms displayed.
this.fastLocation_ = undefined;
var checkInitialLangZoneTerms = 'processLangZoneTerms(true, \'' +
locale + '\', \'' + this.countryCode + '\');';
var details = {code: checkInitialLangZoneTerms};
termsPage.termsView_.executeScript(details, function(results) {});
}
}
}
/** Called when the terms-view loading is aborted. */
onTermsViewLoadAborted_(reason) {
console.error('TermsView loading is aborted: ' + reason);
// Mark ABORTED so that onTermsViewLoaded_() won't show the content view.
this.fastLocation_ = undefined;
this.state_ = LoadState.ABORTED;
showErrorPage(
appWindow.contentWindow.loadTimeData.getString('serverError'));
}
/** Called when the terms-view's load request is completed. */
onTermsViewRequestCompleted_(details) {
if (this.state_ != LoadState.LOADING || details.statusCode == 200) {
return;
}
// In case we failed with fast location let retry default scheme.
if (this.fastLocation_) {
this.fastLocation_ = undefined;
this.termsView_.src = 'https://play.google.com/about/play-terms/';
return;
}
this.onTermsViewLoadAborted_(
'request failed with status ' + details.statusCode);
}
/** Called when "AGREE" button is clicked. */
onAgree() {
sendNativeMessage('onAgreed', this.getPageResults_());
}
/** Called when "CANCEL" button is clicked. */
onCancel_() {
sendNativeMessage('onCanceled', this.getPageResults_());
closeWindow();
}
/** Called when metrics preference is updated. */
onMetricsPreferenceChanged(isEnabled, isManaged) {
this.metricsCheckbox_.onPreferenceChanged(isEnabled, isManaged);
// Applying metrics mode may change page layout, update terms height.
this.updateTermsHeight_();
}
/** Called when backup-restore preference is updated. */
onBackupRestorePreferenceChanged(isEnabled, isManaged) {
this.backupRestoreCheckbox_.onPreferenceChanged(isEnabled, isManaged);
}
/** Called when location service preference is updated. */
onLocationServicePreferenceChanged(isEnabled, isManaged) {
this.locationServiceCheckbox_.onPreferenceChanged(isEnabled, isManaged);
}
}
/**
* Handles events for the Active Directory authentication page.
*/
class ActiveDirectoryAuthPage {
/**
* @param {Element} container The container of the page.
*/
constructor(container) {
var requestFilter = {urls: ['<all_urls>'], types: ['main_frame']};
this.authView_ = container.querySelector('#active-directory-auth-view');
this.authView_.request.onCompleted.addListener(
(details) => this.onAuthViewCompleted_(details), requestFilter);
this.authView_.request.onErrorOccurred.addListener(
(details) => this.onAuthViewErrorOccurred_(details), requestFilter);
this.deviceManagementUrlPrefix_ = null;
// https://crbug.com/756144: Disable event processing while the page is not
// shown. The bug seems to be caused by erroneous onErrorOccurred events
// that are fired even though authView_.src is never set. This might be
// related to a bug in webview, see also CL:638413.
this.process_events_ = false;
container.querySelector('#button-active-directory-auth-cancel')
.addEventListener('click', () => this.onCancel_());
}
/**
* Sets URLs used for Active Directory user SAML authentication.
* @param {string} federationUrl The Active Directory Federation Services URL.
* @param {string} deviceManagementUrlPrefix Device management server URL
* prefix used to detect if the SAML flow finished. DM server is the
* SAML service provider.
*/
setUrls(federationUrl, deviceManagementUrlPrefix) {
this.authView_.src = federationUrl;
this.deviceManagementUrlPrefix_ = deviceManagementUrlPrefix;
}
/**
* Toggles onCompleted and onErrorOccurred event processing.
* @param {boolean} enabled Process (true) or ignore (false) events.
*/
enableEventProcessing(enabled) {
this.process_events_ = enabled;
}
/**
* Auth view onCompleted event handler. Checks whether the SAML flow
* reached its endpoint, the device management server.
* @param {!Object} details Event parameters.
*/
onAuthViewCompleted_(details) {
if (!this.process_events_) {
console.error(
'Unexpected onAuthViewCompleted_ event from URL ' + details.url);
return;
}
// See if we hit the device management server. This should happen at the
// end of the SAML flow. Before that, we're on the Active Directory
// Federation Services server.
if (this.deviceManagementUrlPrefix_ &&
details.url.startsWith(this.deviceManagementUrlPrefix_)) {
// Once we hit the final URL, stop processing further events.
this.process_events_ = false;
// Did it actually work?
if (details.statusCode == 200) {
// 'code' is unused, but it needs to be there.
sendNativeMessage('onAuthSucceeded');
} else {
sendNativeMessage('onAuthFailed', {
errorMessage:
'Status code ' + details.statusCode + ' in DM server response.'
});
}
}
}
/**
* Auth view onErrorOccurred event handler.
* @param {!Object} details Event parameters.
*/
onAuthViewErrorOccurred_(details) {
if (!this.process_events_) {
console.error(
'Unexpected onAuthViewErrorOccurred_ event: ' + details.error);
return;
}
// Retry triggers net::ERR_ABORTED, so ignore it.
if (details.error == 'net::ERR_ABORTED')
return;
// Stop processing further events on first error.
this.process_events_ = false;
sendNativeMessage(
'onAuthFailed', {errorMessage: 'Error occurred: ' + details.error});
}
/** Called when the "CANCEL" button is clicked. */
onCancel_() {
closeWindow();
}
}
/**
* Applies localization for html content and sets terms webview.
* @param {!Object} data Localized strings and relevant information.
* @param {string} deviceId Current device id.
*/
function initialize(data, deviceId) {
currentDeviceId = deviceId;
var doc = appWindow.contentWindow.document;
var loadTimeData = appWindow.contentWindow.loadTimeData;
loadTimeData.data = data;
appWindow.contentWindow.i18nTemplate.process(doc, loadTimeData);
locale = loadTimeData.getString('locale');
// Initialize preference connected checkboxes in terms of service page.
termsPage = new TermsOfServicePage(
doc.getElementById('terms'), data.arcManaged, data.countryCode,
new MetricsPreferenceCheckbox(
doc.getElementById('metrics-preference'), data.learnMoreStatistics,
'#learn-more-link-metrics', data.isOwnerProfile,
data.textMetricsDisabled, data.textMetricsEnabled,
data.textMetricsManagedDisabled, data.textMetricsManagedEnabled),
new PreferenceCheckbox(
doc.getElementById('backup-restore-preference'),
data.learnMoreBackupAndRestore, '#learn-more-link-backup-restore',
data.controlledByPolicy),
new PreferenceCheckbox(
doc.getElementById('location-service-preference'),
data.learnMoreLocationServices, '#learn-more-link-location-service',
data.controlledByPolicy),
data.learnMorePaiService);
// Initialize the Active Directory SAML authentication page.
activeDirectoryAuthPage =
new ActiveDirectoryAuthPage(doc.getElementById('active-directory-auth'));
doc.getElementById('close-button').title =
loadTimeData.getString('overlayClose');
adjustTopMargin();
}
// With UI request to change inner window size to outer window size and reduce
// top spacing, adjust top margin to negtive window top bar height.
function adjustTopMargin() {
if (!appWindow)
return;
var decorationHeight =
appWindow.outerBounds.height - appWindow.innerBounds.height;
var doc = appWindow.contentWindow.document;
var headers = doc.getElementsByClassName('header');
for (var i = 0; i < headers.length; i++) {
headers[i].style.marginTop = -decorationHeight + 'px';
}
var authPages = doc.getElementsByClassName('section-active-directory-auth');
for (var i = 0; i < authPages.length; i++) {
authPages[i].style.marginTop = -decorationHeight + 'px';
}
}
/**
* Handles native messages received from ArcSupportHost.
* @param {!Object} message The message received.
*/
function onNativeMessage(message) {
if (!message.action) {
return;
}
if (!appWindow) {
console.warn('Received native message when window is not available.');
return;
}
if (message.action == 'initialize') {
initialize(message.data, message.deviceId);
} else if (message.action == 'setMetricsMode') {
termsPage.onMetricsPreferenceChanged(message.enabled, message.managed);
} else if (message.action == 'setBackupAndRestoreMode') {
termsPage.onBackupRestorePreferenceChanged(
message.enabled, message.managed);
} else if (message.action == 'setLocationServiceMode') {
termsPage.onLocationServicePreferenceChanged(
message.enabled, message.managed);
} else if (message.action == 'showPage') {
showPage(message.page, message.options);
} else if (message.action == 'showErrorPage') {
showErrorPage(message.errorMessage, message.shouldShowSendFeedback);
} else if (message.action == 'closeWindow') {
closeWindow();
} else if (message.action == 'setWindowBounds') {
setWindowBounds();
}
}
/**
* Connects to ArcSupportHost.
*/
function connectPort() {
var hostName = 'com.google.arc_support';
port = chrome.runtime.connectNative(hostName);
port.onMessage.addListener(onNativeMessage);
}
/**
* Shows requested page and hide others. Show appWindow if it was hidden before.
* 'none' hides all views.
* @param {string} pageDivId id of divider of the page to show.
* @param {dictionary=} options Addional options depending on pageDivId. For
* 'active-directory-auth', this has to contain keys 'federationUrl' and
* 'deviceManagementUrlPrefix' with corresponding values. See
* ActiveDirectoryAuthPage::setUrls for a description of those parameters.
*/
function showPage(pageDivId, options) {
if (!appWindow) {
return;
}
hideOverlay();
appWindow.contentWindow.stopProgressAnimation();
var doc = appWindow.contentWindow.document;
var pages = doc.getElementsByClassName('section');
for (var i = 0; i < pages.length; i++) {
pages[i].hidden = pages[i].id != pageDivId;
}
if (pageDivId == 'active-directory-auth') {
activeDirectoryAuthPage.enableEventProcessing(true);
activeDirectoryAuthPage.setUrls(
options.federationUrl, options.deviceManagementUrlPrefix);
} else {
activeDirectoryAuthPage.enableEventProcessing(false);
}
appWindow.show();
if (pageDivId == 'terms') {
termsPage.onShow();
}
// Start progress bar animation for the page that has the dynamic progress
// bar. 'error' page has the static progress bar that no need to be animated.
if (pageDivId == 'terms' || pageDivId == 'arc-loading') {
appWindow.contentWindow.startProgressAnimation(pageDivId);
}
}
/**
* Shows an error page, with given errorMessage.
*
* @param {string} errorMessage Localized error message text.
* @param {?boolean} opt_shouldShowSendFeedback If set to true, show "Send
* feedback" button.
*/
function showErrorPage(errorMessage, opt_shouldShowSendFeedback) {
if (!appWindow) {
return;
}
var doc = appWindow.contentWindow.document;
var messageElement = doc.getElementById('error-message');
messageElement.innerText = errorMessage;
var sendFeedbackElement = doc.getElementById('button-send-feedback');
sendFeedbackElement.hidden = !opt_shouldShowSendFeedback;
showPage('error');
}
/**
* Shows overlay dialog and required content.
* @param {string} overlayClass Defines which content to show, 'overlay-url' for
* webview based content and 'overlay-text' for
* simple text view.
*/
function showOverlay(overlayClass) {
var doc = appWindow.contentWindow.document;
var overlayContainer = doc.getElementById('overlay-container');
overlayContainer.classList.remove('overlay-text');
overlayContainer.classList.remove('overlay-url');
overlayContainer.classList.add('overlay-loading');
overlayContainer.classList.add(overlayClass);
overlayContainer.hidden = false;
lastFocusedElement = doc.activeElement;
doc.getElementById('overlay-close').focus();
}
/**
* Opens overlay dialog and shows formatted text content there.
* @param {string} content HTML formatted text to show.
*/
function showTextOverlay(content) {
var doc = appWindow.contentWindow.document;
var textContent = doc.getElementById('overlay-text-content');
textContent.innerHTML = content;
showOverlay('overlay-text');
}
/**
* Opens overlay dialog and shows external URL there.
* @param {string} url Target URL to open in overlay dialog.
*/
function showURLOverlay(url) {
var doc = appWindow.contentWindow.document;
var overlayWebview = doc.getElementById('overlay-url');
overlayWebview.src = url;
showOverlay('overlay-url');
}
/**
* Shows Google Privacy Policy in overlay dialog. Policy link is detected from
* the content of terms view.
*/
function showPrivacyPolicyOverlay() {
var defaultLink =
'https://www.google.com/intl/' + locale + '/policies/privacy/';
if (termsPage.isManaged_) {
showURLOverlay(defaultLink);
return;
}
var details = {code: 'getPrivacyPolicyLink();'};
termsPage.termsView_.executeScript(details, function(results) {
if (results && results.length == 1 && typeof results[0] == 'string') {
showURLOverlay(results[0]);
} else {
showURLOverlay(defaultLink);
}
});
}
/**
* Hides overlay dialog.
*/
function hideOverlay() {
var doc = appWindow.contentWindow.document;
var overlayContainer = doc.getElementById('overlay-container');
overlayContainer.hidden = true;
if (lastFocusedElement) {
lastFocusedElement.focus();
lastFocusedElement = null;
}
}
function setWindowBounds() {
if (!appWindow) {
return;
}
var outerWidth = OUTER_WIDTH;
var outerHeight = OUTER_HEIGHT;
if (outerWidth > screen.availWidth) {
outerWidth = screen.availWidth;
}
if (outerHeight > screen.availHeight) {
outerHeight = screen.availHeight;
}
if (appWindow.outerBounds.width == outerWidth &&
appWindow.outerBounds.height == outerHeight) {
return;
}
appWindow.outerBounds.width = outerWidth;
appWindow.outerBounds.height = outerHeight;
appWindow.outerBounds.left = Math.ceil((screen.availWidth - outerWidth) / 2);
appWindow.outerBounds.top = Math.ceil((screen.availHeight - outerHeight) / 2);
}
function closeWindow() {
if (appWindow) {
appWindow.close();
}
}
chrome.app.runtime.onLaunched.addListener(function() {
var onAppContentLoad = function() {
var onRetry = function() {
sendNativeMessage('onRetryClicked');
};
var onSendFeedback = function() {
sendNativeMessage('onSendFeedbackClicked');
};
var doc = appWindow.contentWindow.document;
doc.getElementById('button-retry').addEventListener('click', onRetry);
doc.getElementById('button-send-feedback')
.addEventListener('click', onSendFeedback);
doc.getElementById('overlay-close').addEventListener('click', hideOverlay);
doc.getElementById('privacy-policy-link')
.addEventListener('click', showPrivacyPolicyOverlay);
var overlay = doc.getElementById('overlay-container');
appWindow.contentWindow.cr.ui.overlay.setupOverlay(overlay);
appWindow.contentWindow.cr.ui.overlay.globalInitialization();
overlay.addEventListener('cancelOverlay', hideOverlay);
var overlayWebview = doc.getElementById('overlay-url');
overlayWebview.addEventListener('contentload', function() {
overlay.classList.remove('overlay-loading');
});
overlayWebview.addContentScripts([{
name: 'postProcess',
matches: ['https://support.google.com/*'],
css: {files: ['overlay.css']},
run_at: 'document_end'
}]);
focusManager = new appWindow.contentWindow.ArcOptInFocusManager();
focusManager.initialize();
connectPort();
};
var onWindowCreated = function(createdWindow) {
appWindow = createdWindow;
appWindow.contentWindow.onload = onAppContentLoad;
appWindow.onClosed.addListener(onWindowClosed);
setWindowBounds();
};
var onWindowClosed = function() {
appWindow = null;
// Turn off event processing.
activeDirectoryAuthPage.enableEventProcessing(false);
// Notify to Chrome.
sendNativeMessage('onWindowClosed');
// On window closed, then dispose the extension. So, close the port
// otherwise the background page would be kept alive so that the extension
// would not be unloaded.
port.disconnect();
port = null;
};
var options = {
'id': 'play_store_wnd',
'resizable': false,
'hidden': true,
'frame': {type: 'chrome', color: '#ffffff'},
'outerBounds': {'width': OUTER_WIDTH, 'height': OUTER_HEIGHT}
};
chrome.app.window.create('main.html', options, onWindowCreated);
});