// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview
* Settings subpage for managing Bluetooth properties and devices.
*/
import '../settings_shared.css.js';
import './os_paired_bluetooth_list.js';
import './settings_fast_pair_toggle.js';
import {PrefsMixin} from '/shared/settings/prefs/prefs_mixin.js';
import {BluetoothUiSurface, recordBluetoothUiSurfaceMetrics} from 'chrome://resources/ash/common/bluetooth/bluetooth_metrics_utils.js';
import {getBluetoothConfig} from 'chrome://resources/ash/common/bluetooth/cros_bluetooth_config.js';
import {getHidPreservingController} from 'chrome://resources/ash/common/bluetooth/hid_preserving_bluetooth_state_controller.js';
import {HidWarningDialogSource} from 'chrome://resources/ash/common/bluetooth/hid_preserving_bluetooth_state_controller.mojom-webui.js';
import {getInstance as getAnnouncerInstance} from 'chrome://resources/ash/common/cr_elements/cr_a11y_announcer/cr_a11y_announcer.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {WebUiListenerMixin} from 'chrome://resources/ash/common/cr_elements/web_ui_listener_mixin.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {BluetoothSystemProperties, BluetoothSystemState, DeviceConnectionState, PairedBluetoothDeviceProperties} from 'chrome://resources/mojo/chromeos/ash/services/bluetooth_config/public/mojom/cros_bluetooth_config.mojom-webui.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {DeepLinkingMixin} from '../common/deep_linking_mixin.js';
import {RouteObserverMixin} from '../common/route_observer_mixin.js';
import {Setting} from '../mojom-webui/setting.mojom-webui.js';
import {Route, Router, routes} from '../router.js';
import {getTemplate} from './os_bluetooth_devices_subpage.html.js';
import {OsBluetoothDevicesSubpageBrowserProxy, OsBluetoothDevicesSubpageBrowserProxyImpl} from './os_bluetooth_devices_subpage_browser_proxy.js';
const SettingsBluetoothDevicesSubpageElementBase = DeepLinkingMixin(PrefsMixin(
RouteObserverMixin(WebUiListenerMixin(I18nMixin(PolymerElement)))));
export class SettingsBluetoothDevicesSubpageElement extends
SettingsBluetoothDevicesSubpageElementBase {
static get is() {
return 'os-settings-bluetooth-devices-subpage' as const;
}
static get template() {
return getTemplate();
}
static get properties() {
return {
systemProperties: {
type: Object,
observer: 'onSystemPropertiesChanged_',
},
/**
* Used by DeepLinkingMixin to focus this page's deep links.
*/
supportedSettingIds: {
type: Object,
value: () =>
new Set<Setting>([Setting.kBluetoothOnOff, Setting.kFastPairOnOff]),
},
/**
* Reflects the current state of the toggle button. This will be set when
* the |systemProperties| state changes or when the user presses the
* toggle.
*/
isBluetoothToggleOn_: {
type: Boolean,
observer: 'onIsBluetoothToggleOnChanged_',
},
/**
* Whether or not this device has the requirements to support fast pair.
*/
isFastPairSupportedByDevice_: {
type: Boolean,
value: true,
},
connectedDevices_: {
type: Array,
value: [],
},
savedDevicesSublabel_: {
type: String,
value() {
return loadTimeData.getString('sublabelWithEmail');
},
},
unconnectedDevices_: {
type: Array,
value: [],
},
isBluetoothDisconnectWarningEnabled_: {
type: Boolean,
value() {
return loadTimeData.getBoolean('bluetoothDisconnectWarningFlag');
},
readOnly: true,
},
isFastPairSoftwareScanningSupportEnabled_: {
type: Boolean,
readOnly: true,
value() {
return loadTimeData.getBoolean(
'isFastPairSoftwareScanningSupportEnabled');
},
},
// Consistent with enum `SoftwareScanningStatus` in
// scanning_enabled_provider.h.
menuOptions_: {
type: Array,
readOnly: true,
value() {
return [
{name: 'Never', value: 0},
{name: 'Only when charging', value: 2},
];
},
},
isBatterySaverActive_: {
type: Boolean,
value: false,
},
isHardwareOffloadingSupported_: {
type: Boolean,
value: false,
},
};
}
systemProperties: BluetoothSystemProperties;
private browserProxy_: OsBluetoothDevicesSubpageBrowserProxy;
private connectedDevices_: PairedBluetoothDeviceProperties[];
private isBluetoothToggleOn_: boolean;
private isFastPairSupportedByDevice_: boolean;
private lastSelectedDeviceId_: string|null;
private readonly menuOptions_: string[];
private savedDevicesSublabel_: string;
private unconnectedDevices_: PairedBluetoothDeviceProperties[];
private isBluetoothDisconnectWarningEnabled_: boolean;
private readonly isFastPairSoftwareScanningSupportEnabled_: boolean;
private isBatterySaverActive_: boolean;
private isHardwareOffloadingSupported_: boolean;
constructor() {
super();
/**
* The id of the last device that was selected to view its detail page.
*/
this.lastSelectedDeviceId_ = null;
this.browserProxy_ =
OsBluetoothDevicesSubpageBrowserProxyImpl.getInstance();
}
override ready(): void {
super.ready();
if (loadTimeData.getBoolean('enableFastPairFlag')) {
this.addWebUiListener(
'fast-pair-device-supported-status', (isSupported: boolean) => {
this.isFastPairSupportedByDevice_ = isSupported;
});
this.browserProxy_.requestFastPairDeviceSupport();
}
if (loadTimeData.getBoolean('isFastPairSoftwareScanningSupportEnabled')) {
// Listen for changes in Battery Saver status.
this.addWebUiListener(
'fast-pair-software-scanning-battery-saver-status',
(isBatterySaverActive: boolean) => {
this.isBatterySaverActive_ = isBatterySaverActive;
});
this.browserProxy_.requestBatterySaverStatus();
// Listen for changes in Hardware Offloading Support status.
this.addWebUiListener(
'fast-pair-software-scanning-hardware-offloading-status',
(isHardwareOffloadingSupported: boolean) => {
this.isHardwareOffloadingSupported_ = isHardwareOffloadingSupported;
});
this.browserProxy_.requestHardwareOffloadingSupportStatus();
}
}
/**
* RouteObserverMixin override
*/
override currentRouteChanged(route: Route, oldRoute?: Route): void {
// If we're navigating to a device's detail page, save the id of the device.
if (route === routes.BLUETOOTH_DEVICE_DETAIL &&
oldRoute === routes.BLUETOOTH_DEVICES) {
const queryParams = Router.getInstance().getQueryParameters();
this.lastSelectedDeviceId_ = queryParams.get('id');
return;
}
if (route !== routes.BLUETOOTH_DEVICES) {
return;
}
recordBluetoothUiSurfaceMetrics(
BluetoothUiSurface.SETTINGS_DEVICE_LIST_SUBPAGE);
this.browserProxy_.showBluetoothRevampHatsSurvey();
this.attemptDeepLink();
// If a backwards navigation occurred from a Bluetooth device's detail page,
// focus the list item corresponding to that device.
if (oldRoute !== routes.BLUETOOTH_DEVICE_DETAIL) {
return;
}
// Don't attempt to focus any item unless the last navigation was a
// 'pop' (backwards) navigation.
if (!Router.getInstance().lastRouteChangeWasPopstate()) {
return;
}
this.focusLastSelectedDeviceItem_();
}
private onSystemPropertiesChanged_(): void {
this.isBluetoothToggleOn_ =
this.systemProperties.systemState === BluetoothSystemState.kEnabled ||
this.systemProperties.systemState === BluetoothSystemState.kEnabling;
this.connectedDevices_ = this.systemProperties.pairedDevices.filter(
device => device.deviceProperties.connectionState ===
DeviceConnectionState.kConnected);
this.unconnectedDevices_ = this.systemProperties.pairedDevices.filter(
device => device.deviceProperties.connectionState !==
DeviceConnectionState.kConnected);
}
private focusLastSelectedDeviceItem_(): void {
const focusItem = (deviceListSelector: string, index: number): void => {
const deviceList =
this.shadowRoot!.querySelector<HTMLElement>(deviceListSelector);
const items = deviceList!.shadowRoot!.querySelectorAll(
'os-settings-paired-bluetooth-list-item');
if (index >= items.length) {
return;
}
items[index].focus();
};
// Search |connectedDevices_| for the device.
let index = this.connectedDevices_.findIndex(
device => device.deviceProperties.id === this.lastSelectedDeviceId_);
if (index >= 0) {
focusItem(/*deviceListSelector=*/ '#connectedDeviceList', index);
return;
}
// If |connectedDevices_| doesn't contain the device, search
// |unconnectedDevices_|.
index = this.unconnectedDevices_.findIndex(
device => device.deviceProperties.id === this.lastSelectedDeviceId_);
if (index < 0) {
return;
}
focusItem(/*deviceListSelector=*/ '#unconnectedDeviceList', index);
}
/**
* Observer for isBluetoothToggleOn_ that returns early until the previous
* value was not undefined to avoid wrongly toggling the Bluetooth state.
*/
private onIsBluetoothToggleOnChanged_(_newValue: boolean, oldValue?: boolean):
void {
if (oldValue === undefined) {
return;
}
this.announceBluetoothStateChange_();
}
private isToggleDisabled_(): boolean {
// TODO(crbug.com/1010321): Add check for modification state when variable
// is available.
return this.systemProperties.systemState ===
BluetoothSystemState.kUnavailable;
}
private getOnOffString_(
isBluetoothToggleOn: boolean, onString: string,
offString: string): string {
return isBluetoothToggleOn ? onString : offString;
}
private shouldShowDeviceList_(devices: PairedBluetoothDeviceProperties[]):
boolean {
return devices.length > 0;
}
private shouldShowNoDevicesFound_(): boolean {
return !this.connectedDevices_.length && !this.unconnectedDevices_.length;
}
private announceBluetoothStateChange_(): void {
getAnnouncerInstance().announce(
this.isBluetoothToggleOn_ ? this.i18n('bluetoothEnabledA11YLabel') :
this.i18n('bluetoothDisabledA11YLabel'));
}
private isFastPairToggleVisible_(): boolean {
return this.isFastPairSupportedByDevice_ &&
loadTimeData.getBoolean('enableFastPairFlag');
}
private onBluetoothToggleChange_(event: CustomEvent): void {
event.stopPropagation();
// If the toggle value changed but the toggle is disabled, the change came
// from CrosBluetoothConfig, not the user. Don't attempt to update the
// enabled state.
if (this.isToggleDisabled_()) {
return;
}
const enabled = event.detail;
if (this.isBluetoothDisconnectWarningEnabled_) {
// Reset Bluetooth toggle state to previous state. Toggle should only be
// updated when System properties changes.
this.isBluetoothToggleOn_ = !enabled;
getHidPreservingController().tryToSetBluetoothEnabledState(
enabled, HidWarningDialogSource.kOsSettings);
} else {
getBluetoothConfig().setBluetoothEnabledState(enabled);
}
}
/**
* Determines if we allow access to the Saved Devices page. Unlike the Fast
* Pair toggle, the device does not need to support Fast Pair because a device
* could be saved to the user's account from a different device but managed on
* this device. However Fast Pair must be enabled to confirm we have all Fast
* Pair (and Saved Device) related code working on the device.
*/
private isFastPairSavedDevicesRowVisible_(): boolean {
return loadTimeData.getBoolean('enableFastPairFlag') &&
loadTimeData.getBoolean('enableSavedDevicesFlag') &&
!loadTimeData.getBoolean('isGuest') &&
loadTimeData.getBoolean('isCrossDeviceFeatureSuiteEnabled');
}
private onClicked_(event: Event): void {
Router.getInstance().navigateTo(routes.BLUETOOTH_SAVED_DEVICES);
event.stopPropagation();
}
}
declare global {
interface HTMLElementTagNameMap {
[SettingsBluetoothDevicesSubpageElement.is]:
SettingsBluetoothDevicesSubpageElement;
}
}
customElements.define(
SettingsBluetoothDevicesSubpageElement.is,
SettingsBluetoothDevicesSubpageElement);