| // Copyright 2017 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. |
| |
| Polymer({ |
| is: 'settings-multidevice-page', |
| |
| behaviors: [ |
| DeepLinkingBehavior, |
| settings.RouteObserverBehavior, |
| MultiDeviceFeatureBehavior, |
| WebUIListenerBehavior, |
| PrefsBehavior, |
| ], |
| |
| properties: { |
| /** Preferences state. */ |
| prefs: {type: Object}, |
| |
| /** |
| * A Map specifying which element should be focused when exiting a subpage. |
| * The key of the map holds a settings.Route path, and the value holds a |
| * query selector that identifies the desired element. |
| * @private {!Map<string, string>} |
| */ |
| focusConfig_: { |
| type: Object, |
| value() { |
| const map = new Map(); |
| if (settings.routes.MULTIDEVICE_FEATURES) { |
| map.set( |
| settings.routes.MULTIDEVICE_FEATURES.path, |
| '#multidevice-item .subpage-arrow'); |
| } |
| return map; |
| }, |
| }, |
| |
| /** |
| * Authentication token provided by password-prompt-dialog. |
| * @private {!chrome.quickUnlockPrivate.TokenInfo|undefined} |
| */ |
| authToken_: { |
| type: Object, |
| }, |
| |
| /** |
| * Feature which the user has requested to be enabled but could not be |
| * enabled immediately because authentication (i.e., entering a password) is |
| * required. This value is initialized to null, is set when the password |
| * dialog is opened, and is reset to null again once the password dialog is |
| * closed. |
| * @private {?settings.MultiDeviceFeature} |
| */ |
| featureToBeEnabledOnceAuthenticated_: { |
| type: Number, |
| value: null, |
| }, |
| |
| /** @private {boolean} */ |
| showPasswordPromptDialog_: { |
| type: Boolean, |
| value: false, |
| }, |
| |
| /** @private {boolean} */ |
| showNotificationAccessSetupDialog_: { |
| type: Boolean, |
| value: false, |
| }, |
| |
| /** |
| * Whether or not Nearby Share is supported which controls if the Nearby |
| * Share settings and subpage are accessible. |
| * @private {boolean} |
| */ |
| isNearbyShareSupported_: { |
| type: Boolean, |
| value: function() { |
| return loadTimeData.getBoolean('isNearbyShareSupported'); |
| } |
| }, |
| |
| /** |
| * Used by DeepLinkingBehavior to focus this page's deep links. |
| * @type {!Set<!chromeos.settings.mojom.Setting>} |
| */ |
| supportedSettingIds: { |
| type: Object, |
| value: () => new Set([ |
| chromeos.settings.mojom.Setting.kSetUpMultiDevice, |
| chromeos.settings.mojom.Setting.kVerifyMultiDeviceSetup, |
| chromeos.settings.mojom.Setting.kMultiDeviceOnOff, |
| chromeos.settings.mojom.Setting.kNearbyShareOnOff, |
| ]), |
| }, |
| }, |
| |
| listeners: { |
| 'close': 'onDialogClose_', |
| 'feature-toggle-clicked': 'onFeatureToggleClicked_', |
| 'forget-device-requested': 'onForgetDeviceRequested_', |
| }, |
| |
| /** @private {?settings.MultiDeviceBrowserProxy} */ |
| browserProxy_: null, |
| |
| /** @override */ |
| ready() { |
| this.browserProxy_ = settings.MultiDeviceBrowserProxyImpl.getInstance(); |
| |
| this.addWebUIListener( |
| 'settings.updateMultidevicePageContentData', |
| this.onPageContentDataChanged_.bind(this)); |
| |
| this.browserProxy_.getPageContentData().then( |
| this.onInitialPageContentDataFetched_.bind(this)); |
| }, |
| |
| /** |
| * Overridden from settings.RouteObserverBehavior. |
| * @param {!settings.Route} route |
| * @param {!settings.Route} oldRoute |
| * @protected |
| */ |
| currentRouteChanged(route, oldRoute) { |
| this.leaveNestedPageIfNoHostIsSet_(); |
| |
| // Does not apply to this page. |
| if (route !== settings.routes.MULTIDEVICE) { |
| return; |
| } |
| |
| this.attemptDeepLink(); |
| }, |
| |
| /** |
| * @return {string} Translated item label. |
| * @private |
| */ |
| getLabelText_() { |
| return this.pageContentData.hostDeviceName || |
| this.i18n('multideviceSetupItemHeading'); |
| }, |
| |
| /** |
| * @return {string} Translated sublabel with a "learn more" link. |
| * @private |
| */ |
| getSubLabelInnerHtml_() { |
| if (!this.isSuiteAllowedByPolicy()) { |
| return this.i18nAdvanced('multideviceSetupSummary'); |
| } |
| switch (this.pageContentData.mode) { |
| case settings.MultiDeviceSettingsMode.NO_ELIGIBLE_HOSTS: |
| return this.i18nAdvanced('multideviceNoHostText'); |
| case settings.MultiDeviceSettingsMode.NO_HOST_SET: |
| return this.i18nAdvanced('multideviceSetupSummary'); |
| case settings.MultiDeviceSettingsMode.HOST_SET_WAITING_FOR_SERVER: |
| // Intentional fall-through. |
| case settings.MultiDeviceSettingsMode.HOST_SET_WAITING_FOR_VERIFICATION: |
| return this.i18nAdvanced('multideviceVerificationText'); |
| default: |
| return this.isSuiteOn() ? this.i18n('multideviceEnabled') : |
| this.i18n('multideviceDisabled'); |
| } |
| }, |
| |
| /** |
| * @return {string} Translated button text. |
| * @private |
| */ |
| getButtonText_() { |
| switch (this.pageContentData.mode) { |
| case settings.MultiDeviceSettingsMode.NO_HOST_SET: |
| return this.i18n('multideviceSetupButton'); |
| case settings.MultiDeviceSettingsMode.HOST_SET_WAITING_FOR_SERVER: |
| // Intentional fall-through. |
| case settings.MultiDeviceSettingsMode.HOST_SET_WAITING_FOR_VERIFICATION: |
| return this.i18n('multideviceVerifyButton'); |
| default: |
| return ''; |
| } |
| }, |
| |
| /** |
| * @return {string} "true" or "false" indicating whether the text box |
| * should be aria-hidden or not. |
| * @private |
| */ |
| getTextAriaHidden_() { |
| // When host is set and verified, we only show subpage arrow button and |
| // toggle. In this case, we avoid the navigation stops on the text to make |
| // navigating easier. The arrow button is labeled and described by the text, |
| // so the text is still available to assistive tools. |
| return String( |
| this.pageContentData.mode === |
| settings.MultiDeviceSettingsMode.HOST_SET_VERIFIED); |
| }, |
| |
| /** |
| * @return {boolean} |
| * @private |
| */ |
| shouldShowButton_() { |
| return [ |
| settings.MultiDeviceSettingsMode.NO_HOST_SET, |
| settings.MultiDeviceSettingsMode.HOST_SET_WAITING_FOR_SERVER, |
| settings.MultiDeviceSettingsMode.HOST_SET_WAITING_FOR_VERIFICATION, |
| ].includes(this.pageContentData.mode); |
| }, |
| |
| /** |
| * @return {boolean} |
| * @private |
| */ |
| shouldShowToggle_() { |
| return this.pageContentData.mode === |
| settings.MultiDeviceSettingsMode.HOST_SET_VERIFIED; |
| }, |
| |
| /** |
| * Whether to show the separator bar and, if the state calls for a chevron |
| * (a.k.a. subpage arrow) routing to the subpage, the chevron. |
| * @return {boolean} |
| * @private |
| */ |
| shouldShowSeparatorAndSubpageArrow_() { |
| return this.pageContentData.mode !== |
| settings.MultiDeviceSettingsMode.NO_ELIGIBLE_HOSTS; |
| }, |
| |
| /** |
| * @return {boolean} |
| * @private |
| */ |
| doesClickOpenSubpage_() { |
| return this.isHostSet(); |
| }, |
| |
| /** @private */ |
| handleItemClick_(event) { |
| // We do not open the subpage if the click was on a link. |
| if (event.path[0].tagName === 'A') { |
| event.stopPropagation(); |
| return; |
| } |
| |
| if (!this.isHostSet()) { |
| return; |
| } |
| |
| settings.Router.getInstance().navigateTo( |
| settings.routes.MULTIDEVICE_FEATURES); |
| }, |
| |
| /** @private */ |
| handleButtonClick_(event) { |
| event.stopPropagation(); |
| switch (this.pageContentData.mode) { |
| case settings.MultiDeviceSettingsMode.NO_HOST_SET: |
| this.browserProxy_.showMultiDeviceSetupDialog(); |
| return; |
| case settings.MultiDeviceSettingsMode.HOST_SET_WAITING_FOR_SERVER: |
| // Intentional fall-through. |
| case settings.MultiDeviceSettingsMode.HOST_SET_WAITING_FOR_VERIFICATION: |
| // If this device is waiting for action on the server or the host |
| // device, clicking the button should trigger this action. |
| this.browserProxy_.retryPendingHostSetup(); |
| } |
| }, |
| |
| /** @private */ |
| openPasswordPromptDialog_() { |
| this.showPasswordPromptDialog_ = true; |
| }, |
| |
| onDialogClose_(event) { |
| event.stopPropagation(); |
| if (event.path.some( |
| element => element.id === 'multidevicePasswordPrompt')) { |
| this.onPasswordPromptDialogClose_(); |
| } |
| }, |
| |
| /** @private */ |
| onPasswordPromptDialogClose_() { |
| // The password prompt should only be shown when there is a feature waiting |
| // to be enabled. |
| assert(this.featureToBeEnabledOnceAuthenticated_ !== null); |
| |
| // If |this.authToken_| is set when the dialog has been closed, this means |
| // that the user entered the correct password into the dialog. Thus, send |
| // all pending features to be enabled. |
| if (this.authToken_) { |
| this.browserProxy_.setFeatureEnabledState( |
| this.featureToBeEnabledOnceAuthenticated_, true /* enabled */, |
| this.authToken_.token); |
| settings.recordSettingChange(); |
| |
| // Reset |this.authToken_| now that it has been used. This ensures that |
| // users cannot keep an old auth token and reuse it on an subsequent |
| // request. |
| this.authToken_ = undefined; |
| } |
| |
| // Either the feature was enabled above or the user canceled the request by |
| // clicking "Cancel" on the password dialog. Thus, there is no longer a need |
| // to track any pending feature. |
| this.featureToBeEnabledOnceAuthenticated_ = null; |
| |
| // Remove the password prompt dialog from the DOM. |
| this.showPasswordPromptDialog_ = false; |
| }, |
| |
| /** |
| * Attempt to enable the provided feature. If not authenticated (i.e., |
| * |authToken_| is invalid), display the password prompt to begin the |
| * authentication process. |
| * |
| * @param {!CustomEvent<!{ |
| * feature: !settings.MultiDeviceFeature, |
| * enabled: boolean |
| * }>} event |
| * @private |
| */ |
| onFeatureToggleClicked_(event) { |
| const feature = event.detail.feature; |
| const enabled = event.detail.enabled; |
| |
| // If the feature required authentication to be enabled, open the password |
| // prompt dialog. This is required every time the user enables a security- |
| // sensitive feature (i.e., use of stale auth tokens is not acceptable). |
| if (enabled && this.isAuthenticationRequiredToEnable_(feature)) { |
| this.featureToBeEnabledOnceAuthenticated_ = feature; |
| this.openPasswordPromptDialog_(); |
| return; |
| } |
| |
| // If the feature to enable is Phone Hub Notifications, notification access |
| // must have been granted before the feature can be enabled. |
| if (feature === settings.MultiDeviceFeature.PHONE_HUB_NOTIFICATIONS && |
| enabled) { |
| switch (this.pageContentData.notificationAccessStatus) { |
| case settings.PhoneHubNotificationAccessStatus.PROHIBITED: |
| assertNotReached('Cannot enable notification access; prohibited'); |
| return; |
| case settings.PhoneHubNotificationAccessStatus |
| .AVAILABLE_BUT_NOT_GRANTED: |
| this.showNotificationAccessSetupDialog_ = true; |
| return; |
| default: |
| // Fall through and attempt to toggle feature. |
| break; |
| } |
| } |
| |
| // Disabling any feature does not require authentication, and enable some |
| // features does not require authentication. |
| this.browserProxy_.setFeatureEnabledState(feature, enabled); |
| settings.recordSettingChange(); |
| }, |
| |
| /** |
| * @param {!settings.MultiDeviceFeature} feature The feature to enable. |
| * @return {boolean} Whether authentication is required to enable the feature. |
| * @private |
| */ |
| isAuthenticationRequiredToEnable_(feature) { |
| // Enabling SmartLock always requires authentication. |
| if (feature === settings.MultiDeviceFeature.SMART_LOCK) { |
| return true; |
| } |
| |
| // Enabling any feature besides SmartLock and the Better Together suite does |
| // not require authentication. |
| if (feature !== settings.MultiDeviceFeature.BETTER_TOGETHER_SUITE) { |
| return false; |
| } |
| |
| const smartLockState = |
| this.getFeatureState(settings.MultiDeviceFeature.SMART_LOCK); |
| |
| // If the user is enabling the Better Together suite and this change would |
| // result in SmartLock being implicitly enabled, authentication is required. |
| // SmartLock is implicitly enabled if it is only currently not enabled due |
| // to the suite being disabled or due to the SmartLock host device not |
| // having a lock screen set. |
| return smartLockState === |
| settings.MultiDeviceFeatureState.UNAVAILABLE_SUITE_DISABLED || |
| smartLockState === |
| settings.MultiDeviceFeatureState.UNAVAILABLE_INSUFFICIENT_SECURITY; |
| }, |
| |
| /** @private */ |
| onForgetDeviceRequested_() { |
| this.browserProxy_.removeHostDevice(); |
| settings.recordSettingChange(); |
| settings.Router.getInstance().navigateTo(settings.routes.MULTIDEVICE); |
| }, |
| |
| /** |
| * Checks if the user is in a nested page without a host set and, if so, |
| * navigates them back to the main page. |
| * @private |
| */ |
| leaveNestedPageIfNoHostIsSet_() { |
| // Wait for data to arrive. |
| if (!this.pageContentData) { |
| return; |
| } |
| |
| // Host status doesn't matter if we are navigating to Nearby Share |
| // settings. |
| if (settings.routes.NEARBY_SHARE === |
| settings.Router.getInstance().getCurrentRoute()) { |
| return; |
| } |
| |
| // If the user gets to the a nested page without a host (e.g. by clicking a |
| // stale 'existing user' notifications after forgetting their host) we |
| // direct them back to the main settings page. |
| if (settings.routes.MULTIDEVICE !== |
| settings.Router.getInstance().getCurrentRoute() && |
| settings.routes.MULTIDEVICE.contains( |
| settings.Router.getInstance().getCurrentRoute()) && |
| !this.isHostSet()) { |
| // Render MULTIDEVICE page before the MULTIDEVICE_FEATURES has a chance. |
| Polymer.RenderStatus.beforeNextRender(this, () => { |
| settings.Router.getInstance().navigateTo(settings.routes.MULTIDEVICE); |
| }); |
| } |
| }, |
| |
| /** |
| * @param {!settings.MultiDevicePageContentData} newData |
| * @private |
| */ |
| onInitialPageContentDataFetched_(newData) { |
| this.onPageContentDataChanged_(newData); |
| |
| if (this.pageContentData.notificationAccessStatus !== |
| settings.PhoneHubNotificationAccessStatus.AVAILABLE_BUT_NOT_GRANTED) { |
| return; |
| } |
| |
| // Show the notification access dialog if the url contains the correct |
| // param. |
| const urlParams = settings.Router.getInstance().getQueryParameters(); |
| if (urlParams.get('showNotificationAccessSetupDialog') !== null) { |
| this.showNotificationAccessSetupDialog_ = true; |
| } |
| }, |
| |
| /** |
| * @param {!settings.MultiDevicePageContentData} newData |
| * @private |
| */ |
| onPageContentDataChanged_(newData) { |
| this.pageContentData = newData; |
| this.leaveNestedPageIfNoHostIsSet_(); |
| }, |
| |
| /** |
| * @param {!CustomEvent<!chrome.quickUnlockPrivate.TokenInfo>} e |
| * @private |
| */ |
| onTokenObtained_(e) { |
| this.authToken_ = e.detail; |
| }, |
| |
| |
| /** |
| * @param {boolean} state boolean state that determines which string to show |
| * @param {string} onstr string to show when state is true |
| * @param {string} offstr string to show when state is false |
| * @return {string} localized string |
| * @private |
| */ |
| getOnOffString_(state, onstr, offstr) { |
| return state ? onstr : offstr; |
| }, |
| |
| /** |
| * @param {!Event} event |
| * @private |
| */ |
| nearbyShareClick_(event) { |
| const nearbyEnabled = this.getPref('nearby_sharing.enabled').value; |
| const onboardingComplete = |
| this.getPref('nearby_sharing.onboarding_complete').value; |
| let params = undefined; |
| if (!nearbyEnabled) { |
| if (onboardingComplete) { |
| |
| // If we have already run onboarding at least once, we don't need to do |
| // it again, just enabled the feature in place. |
| this.setPrefValue('nearby_sharing.enabled', true); |
| return; |
| } |
| // Otherwise we need to go into the subpage and trigger the onboarding |
| // dialog. |
| params = new URLSearchParams(); |
| params.set('onboarding', ''); |
| } |
| settings.Router.getInstance().navigateTo( |
| settings.routes.NEARBY_SHARE, params); |
| }, |
| |
| /** @private */ |
| onHideNotificationSetupAccessDialog_() { |
| this.showNotificationAccessSetupDialog_ = false; |
| }, |
| |
| /** @private */ |
| handleNearbySetUpClick_() { |
| const params = new URLSearchParams(); |
| params.set('onboarding', ''); |
| // Set by metrics to determine entrypoint for onboarding |
| params.set('entrypoint', 'settings'); |
| settings.Router.getInstance().navigateTo( |
| settings.routes.NEARBY_SHARE, params); |
| }, |
| }); |