blob: a1b710b9398f1818874f3b8ac39cea488ce1410f [file] [log] [blame]
// Copyright 2015 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.
import '//resources/js/util.m.js';
import '//resources/cr_elements/cr_button/cr_button.m.js';
import '//resources/cr_elements/cr_dialog/cr_dialog.m.js';
import '//resources/cr_elements/cr_input/cr_input.m.js';
import '//resources/cr_elements/cr_link_row/cr_link_row.js';
import '//resources/cr_elements/icons.m.js';
import '//resources/cr_elements/shared_style_css.m.js';
import '//resources/cr_elements/shared_vars_css.m.js';
import '//resources/cr_elements/cr_expand_button/cr_expand_button.m.js';
import '//resources/polymer/v3_0/iron-collapse/iron-collapse.js';
import '//resources/polymer/v3_0/iron-icon/iron-icon.js';
import '//resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js';
import './sync_account_control.js';
import './sync_encryption_options.js';
import '../privacy_page/personalization_options.js';
import '../settings_shared_css.js';
import '../settings_vars_css.js';
// <if expr="not chromeos">
import '//resources/cr_elements/cr_toast/cr_toast.m.js';
// </if>
import {assert, assertNotReached} from '//resources/js/assert.m.js';
import {focusWithoutInk} from '//resources/js/cr/ui/focus_without_ink.m.js';
import {WebUIListenerBehavior} from '//resources/js/web_ui_listener_behavior.m.js';
import {flush, html, Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {loadTimeData} from '../i18n_setup.js';
import {Route, RouteObserverBehavior, Router} from '../router.js';
import {PageStatus, StatusAction, SyncBrowserProxy, SyncBrowserProxyImpl, SyncPrefs, SyncStatus} from './sync_browser_proxy.js';
// TODO(rbpotter): Remove this typedef when this file is no longer needed by OS
// Settings.
/**
* @typedef {{
* BASIC: !Route,
* PEOPLE: !Route,
* SYNC: !Route,
* SYNC_ADVANCED: !Route,
* }}
*/
let SyncRoutes;
/** @return {!SyncRoutes} */
function getSyncRoutes() {
const router = Router.getInstance();
return /** @type {!SyncRoutes} */ (router.getRoutes());
}
/**
* @fileoverview
* 'settings-sync-page' is the settings page containing sync settings.
*/
Polymer({
is: 'settings-sync-page',
_template: html`{__html_template__}`,
behaviors: [
WebUIListenerBehavior,
RouteObserverBehavior,
],
properties: {
/**
* Preferences state.
*/
prefs: {
type: Object,
notify: true,
},
/** @private {!Map<string, (string|Function)>} */
focusConfig: {
type: Object,
observer: 'onFocusConfigChange_',
},
/** @private */
pages_: {
type: Object,
value: PageStatus,
readOnly: true,
},
/**
* The current page status. Defaults to |CONFIGURE| such that the searching
* algorithm can search useful content when the page is not visible to the
* user.
* @private {?PageStatus}
*/
pageStatus_: {
type: String,
value: PageStatus.CONFIGURE,
},
/**
* Dictionary defining page visibility.
* TODO(dpapad): Restore the type information here (PrivacyPageVisibility),
* when this file is no longer shared with chrome://os-settings.
*/
pageVisibility: Object,
/**
* The current sync preferences, supplied by SyncBrowserProxy.
* @type {SyncPrefs|undefined}
*/
syncPrefs: {
type: Object,
},
/** @type {SyncStatus} */
syncStatus: {
type: Object,
},
/** @private */
dataEncrypted_: {
type: Boolean,
computed: 'computeDataEncrypted_(syncPrefs.encryptAllData)'
},
/** @private */
encryptionExpanded_: {
type: Boolean,
value: false,
},
/** If true, override |encryptionExpanded_| to be true. */
forceEncryptionExpanded: {
type: Boolean,
value: false,
},
/**
* The existing passphrase input field value.
* @private
*/
existingPassphrase_: {
type: String,
value: '',
},
/** @private */
signedIn_: {
type: Boolean,
value: true,
computed: 'computeSignedIn_(syncStatus.signedIn)',
},
/** @private */
syncDisabledByAdmin_: {
type: Boolean,
value: false,
computed: 'computeSyncDisabledByAdmin_(syncStatus.managed)',
},
/** @private */
syncSectionDisabled_: {
type: Boolean,
value: false,
computed: 'computeSyncSectionDisabled_(' +
'syncStatus.signedIn, syncStatus.disabled, ' +
'syncStatus.hasError, syncStatus.statusAction, ' +
'syncPrefs.trustedVaultKeysRequired)',
},
/** @private */
showSetupCancelDialog_: {
type: Boolean,
value: false,
},
},
observers: [
'expandEncryptionIfNeeded_(dataEncrypted_, forceEncryptionExpanded)',
],
/** @private {?SyncBrowserProxy} */
browserProxy_: null,
/**
* The beforeunload callback is used to show the 'Leave site' dialog. This
* makes sure that the user has the chance to go back and confirm the sync
* opt-in before leaving.
*
* This property is non-null if the user is currently navigated on the sync
* settings route.
*
* @private {?Function}
*/
beforeunloadCallback_: null,
/**
* The unload callback is used to cancel the sync setup when the user hits
* the browser back button after arriving on the page.
* Note: Cases like closing the tab or reloading don't need to be handled,
* because they are already caught in |PeopleHandler::~PeopleHandler|
* from the C++ code.
*
* @private {?Function}
*/
unloadCallback_: null,
/**
* Whether the initial layout for collapsible sections has been computed. It
* is computed only once, the first time the sync status is updated.
* @private {boolean}
*/
collapsibleSectionsInitialized_: false,
/**
* Whether the user decided to abort sync.
* @private {boolean}
*/
didAbort_: true,
/**
* Whether the user confirmed the cancellation of sync.
* @private {boolean}
*/
setupCancelConfirmed_: false,
/** @override */
created() {
this.browserProxy_ = SyncBrowserProxyImpl.getInstance();
},
/** @override */
attached() {
this.addWebUIListener(
'page-status-changed', this.handlePageStatusChanged_.bind(this));
this.addWebUIListener(
'sync-prefs-changed', this.handleSyncPrefsChanged_.bind(this));
const router = Router.getInstance();
if (router.getCurrentRoute() === getSyncRoutes().SYNC) {
this.onNavigateToPage_();
}
},
/** @override */
detached() {
const router = Router.getInstance();
if (getSyncRoutes().SYNC.contains(router.getCurrentRoute())) {
this.onNavigateAwayFromPage_();
}
if (this.beforeunloadCallback_) {
window.removeEventListener('beforeunload', this.beforeunloadCallback_);
this.beforeunloadCallback_ = null;
}
if (this.unloadCallback_) {
window.removeEventListener('unload', this.unloadCallback_);
this.unloadCallback_ = null;
}
},
/**
* Returns the encryption options SettingsSyncEncryptionOptionsElement.
* @return {?SettingsSyncEncryptionOptionsElement}
*/
getEncryptionOptions() {
return /** @type {?SettingsSyncEncryptionOptionsElement} */ (
this.$$('settings-sync-encryption-options'));
},
/**
* Returns the encryption options SettingsPersonalizationOptionsElement.
* @return {?SettingsPersonalizationOptionsElement}
*/
getPersonalizationOptions() {
return /** @type {?SettingsPersonalizationOptionsElement} */ (
this.$$('settings-personalization-options'));
},
/**
* @return {boolean}
* @private
*/
computeSignedIn_() {
return !!this.syncStatus.signedIn;
},
/**
* @return {boolean}
* @private
*/
computeSyncSectionDisabled_() {
return this.syncStatus !== undefined &&
(!this.syncStatus.signedIn || !!this.syncStatus.disabled ||
(!!this.syncStatus.hasError &&
this.syncStatus.statusAction !==
StatusAction.ENTER_PASSPHRASE &&
this.syncStatus.statusAction !==
StatusAction.RETRIEVE_TRUSTED_VAULT_KEYS));
},
/**
* @return {boolean}
* @private
*/
computeSyncDisabledByAdmin_() {
return this.syncStatus !== undefined && !!this.syncStatus.managed;
},
/** @private */
onFocusConfigChange_() {
const router = Router.getInstance();
this.focusConfig.set(getSyncRoutes().SYNC_ADVANCED.path, () => {
focusWithoutInk(assert(this.$$('#sync-advanced-row')));
});
},
/** @private */
onSetupCancelDialogBack_() {
/** @type {!CrDialogElement} */ (this.$$('#setupCancelDialog')).cancel();
chrome.metricsPrivate.recordUserAction(
'Signin_Signin_CancelCancelAdvancedSyncSettings');
},
/** @private */
onSetupCancelDialogConfirm_() {
this.setupCancelConfirmed_ = true;
/** @type {!CrDialogElement} */ (this.$$('#setupCancelDialog')).close();
const router = Router.getInstance();
router.navigateTo(getSyncRoutes().BASIC);
chrome.metricsPrivate.recordUserAction(
'Signin_Signin_ConfirmCancelAdvancedSyncSettings');
},
/** @private */
onSetupCancelDialogClose_() {
this.showSetupCancelDialog_ = false;
},
/** @protected */
currentRouteChanged() {
const router = Router.getInstance();
if (router.getCurrentRoute() === getSyncRoutes().SYNC) {
this.onNavigateToPage_();
return;
}
if (getSyncRoutes().SYNC.contains(router.getCurrentRoute())) {
return;
}
const searchParams =
Router.getInstance().getQueryParameters().get('search');
if (searchParams) {
// User navigated away via searching. Cancel sync without showing
// confirmation dialog.
this.onNavigateAwayFromPage_();
return;
}
const userActionCancelsSetup = this.syncStatus &&
this.syncStatus.firstSetupInProgress && this.didAbort_;
if (userActionCancelsSetup && !this.setupCancelConfirmed_) {
chrome.metricsPrivate.recordUserAction(
'Signin_Signin_BackOnAdvancedSyncSettings');
// Show the 'Cancel sync?' dialog.
// Yield so that other |currentRouteChanged| observers are called,
// before triggering another navigation (and another round of observers
// firing). Triggering navigation from within an observer leads to some
// undefined behavior and runtime errors.
requestAnimationFrame(() => {
router.navigateTo(getSyncRoutes().SYNC);
this.showSetupCancelDialog_ = true;
// Flush to make sure that the setup cancel dialog is attached.
flush();
this.$$('#setupCancelDialog').showModal();
});
return;
}
// Reset variable.
this.setupCancelConfirmed_ = false;
this.onNavigateAwayFromPage_();
},
/**
* @param {!PageStatus} expectedPageStatus
* @return {boolean}
* @private
*/
isStatus_(expectedPageStatus) {
return expectedPageStatus === this.pageStatus_;
},
/** @private */
onNavigateToPage_() {
const router = Router.getInstance();
assert(router.getCurrentRoute() === getSyncRoutes().SYNC);
if (this.beforeunloadCallback_) {
return;
}
this.collapsibleSectionsInitialized_ = false;
// Display loading page until the settings have been retrieved.
this.pageStatus_ = PageStatus.SPINNER;
this.browserProxy_.didNavigateToSyncPage();
this.beforeunloadCallback_ = event => {
// When the user tries to leave the sync setup, show the 'Leave site'
// dialog.
if (this.syncStatus && this.syncStatus.firstSetupInProgress) {
event.preventDefault();
event.returnValue = '';
chrome.metricsPrivate.recordUserAction(
'Signin_Signin_AbortAdvancedSyncSettings');
}
};
window.addEventListener('beforeunload', this.beforeunloadCallback_);
this.unloadCallback_ = this.onNavigateAwayFromPage_.bind(this);
window.addEventListener('unload', this.unloadCallback_);
},
/** @private */
onNavigateAwayFromPage_() {
if (!this.beforeunloadCallback_) {
return;
}
// Reset the status to CONFIGURE such that the searching algorithm can
// search useful content when the page is not visible to the user.
this.pageStatus_ = PageStatus.CONFIGURE;
this.browserProxy_.didNavigateAwayFromSyncPage(this.didAbort_);
window.removeEventListener('beforeunload', this.beforeunloadCallback_);
this.beforeunloadCallback_ = null;
if (this.unloadCallback_) {
window.removeEventListener('unload', this.unloadCallback_);
this.unloadCallback_ = null;
}
},
/**
* Handler for when the sync preferences are updated.
* @private
*/
handleSyncPrefsChanged_(syncPrefs) {
this.syncPrefs = syncPrefs;
this.pageStatus_ = PageStatus.CONFIGURE;
// Hide the new passphrase box if (a) full data encryption is enabled,
// (b) encrypting all data is not allowed (so far, only applies to
// supervised accounts), or (c) the user is a supervised account.
if (this.syncPrefs.encryptAllData ||
!this.syncPrefs.customPassphraseAllowed ||
(this.syncStatus && this.syncStatus.supervisedUser)) {
this.creatingNewPassphrase_ = false;
}
},
/** @private */
onActivityControlsClick_() {
chrome.metricsPrivate.recordUserAction('Sync_OpenActivityControlsPage');
this.browserProxy_.openActivityControlsUrl();
window.open(loadTimeData.getString('activityControlsUrl'));
},
/** @private */
onSyncDashboardLinkClick_() {
window.open(loadTimeData.getString('syncDashboardUrl'));
},
/**
* @return {boolean}
* @private
*/
computeDataEncrypted_() {
return !!this.syncPrefs && this.syncPrefs.encryptAllData;
},
/**
* Whether the encryption dropdown should be expanded by default.
* @private
*/
expandEncryptionIfNeeded_() {
// Force the dropdown to expand.
if (this.forceEncryptionExpanded) {
this.forceEncryptionExpanded = false;
this.encryptionExpanded_ = true;
return;
}
this.encryptionExpanded_ = this.dataEncrypted_;
},
/**
* @param {!Event} event
* @private
*/
onResetSyncClick_(event) {
if (event.target.tagName === 'A') {
// Stop the propagation of events as the |cr-expand-button|
// prevents the default which will prevent the navigation to the link.
event.stopPropagation();
}
},
/**
* Sends the user-entered existing password to re-enable sync.
* @private
* @param {!Event} e
*/
onSubmitExistingPassphraseTap_(e) {
if (e.type === 'keypress' && e.key !== 'Enter') {
return;
}
this.browserProxy_.setDecryptionPassphrase(this.existingPassphrase_)
.then(
sucessfullySet => this.handlePageStatusChanged_(
sucessfullySet ? PageStatus.DONE :
PageStatus.PASSPHRASE_FAILED));
this.existingPassphrase_ = '';
},
/**
* @private
* @param {!CustomEvent<!{didChange: boolean}>} e
*/
onPassphraseChanged_(e) {
this.handlePageStatusChanged_(
e.detail.didChange ? PageStatus.DONE :
PageStatus.PASSPHRASE_FAILED);
},
/**
* Called when the page status updates.
* @param {!PageStatus} pageStatus
* @private
*/
handlePageStatusChanged_(pageStatus) {
const router = Router.getInstance();
switch (pageStatus) {
case PageStatus.SPINNER:
case PageStatus.CONFIGURE:
this.pageStatus_ = pageStatus;
return;
case PageStatus.DONE:
if (router.getCurrentRoute() === getSyncRoutes().SYNC) {
router.navigateTo(getSyncRoutes().PEOPLE);
}
return;
case PageStatus.PASSPHRASE_FAILED:
if (this.pageStatus_ === this.pages_.CONFIGURE && this.syncPrefs &&
this.syncPrefs.passphraseRequired) {
const passphraseInput = /** @type {!CrInputElement} */ (
this.$$('#existingPassphraseInput'));
passphraseInput.invalid = true;
passphraseInput.focusInput();
}
return;
}
assertNotReached();
},
/**
* @param {!Event} event
* @private
*/
onLearnMoreTap_(event) {
if (event.target.tagName === 'A') {
// Stop the propagation of events, so that clicking on links inside
// checkboxes or radio buttons won't change the value.
event.stopPropagation();
}
},
/**
* @return {boolean}
* @private
*/
shouldShowSyncAccountControl_() {
// <if expr="chromeos">
if (!loadTimeData.getBoolean('useBrowserSyncConsent')) {
return false;
}
// </if>
return this.syncStatus !== undefined &&
!!this.syncStatus.syncSystemEnabled &&
loadTimeData.getBoolean('signinAllowed');
},
/**
* @return {boolean}
* @private
*/
shouldShowExistingPassphraseBelowAccount_() {
return this.syncPrefs !== undefined && !!this.syncPrefs.passphraseRequired;
},
/** @private */
onSyncAdvancedClick_() {
const router = Router.getInstance();
router.navigateTo(getSyncRoutes().SYNC_ADVANCED);
},
/**
* @param {!CustomEvent<boolean>} e The event passed from
* settings-sync-account-control.
* @private
*/
onSyncSetupDone_(e) {
if (e.detail) {
this.didAbort_ = false;
chrome.metricsPrivate.recordUserAction(
'Signin_Signin_ConfirmAdvancedSyncSettings');
} else {
this.setupCancelConfirmed_ = true;
chrome.metricsPrivate.recordUserAction(
'Signin_Signin_CancelAdvancedSyncSettings');
}
const router = Router.getInstance();
router.navigateTo(getSyncRoutes().BASIC);
},
/**
* Focuses the passphrase input element if it is available and the page is
* visible.
* @private
*/
focusPassphraseInput_() {
const passphraseInput =
/** @type {!CrInputElement} */ (this.$$('#existingPassphraseInput'));
const router = Router.getInstance();
if (passphraseInput && router.getCurrentRoute() === getSyncRoutes().SYNC) {
passphraseInput.focus();
}
},
});