blob: ba1f75996fd9d78cc0ed63f556702ed8d6e2917c [file] [log] [blame]
// Copyright 2021 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.
/**
* @fileoverview
* Settings subpage for managing Bluetooth device detail. This Element should
* only be called when a device exist.
*/
import '../../settings_shared.css.js';
import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/cr_elements/policy/cr_tooltip_icon.m.js';
import './os_bluetooth_change_device_name_dialog.js';
import './os_bluetooth_true_wireless_images.js';
import 'chrome://resources/cr_components/chromeos/bluetooth/bluetooth_device_battery_info.js';
import {BluetoothUiSurface, recordBluetoothUiSurfaceMetrics} from 'chrome://resources/cr_components/chromeos/bluetooth/bluetooth_metrics_utils.js';
import {BatteryType} from 'chrome://resources/cr_components/chromeos/bluetooth/bluetooth_types.js';
import {getBatteryPercentage, getDeviceName, hasAnyDetailedBatteryInfo, hasDefaultImage, hasTrueWirelessImages} from 'chrome://resources/cr_components/chromeos/bluetooth/bluetooth_utils.js';
import {getBluetoothConfig} from 'chrome://resources/cr_components/chromeos/bluetooth/cros_bluetooth_config.js';
import {assertNotReached} from 'chrome://resources/js/assert.m.js';
import {I18nBehavior, I18nBehaviorInterface} from 'chrome://resources/js/i18n_behavior.m.js';
import {WebUIListenerBehavior, WebUIListenerBehaviorInterface} from 'chrome://resources/js/web_ui_listener_behavior.m.js';
import {AudioOutputCapability, BluetoothSystemProperties, DeviceConnectionState, DeviceType, PairedBluetoothDeviceProperties} from 'chrome://resources/mojo/chromeos/services/bluetooth_config/public/mojom/cros_bluetooth_config.mojom-webui.js';
import {html, mixinBehaviors, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {Route, Router} from '../../router.js';
import {routes} from '../os_route.js';
import {RouteObserverBehavior, RouteObserverBehaviorInterface} from '../route_observer_behavior.js';
import {RouteOriginBehavior, RouteOriginBehaviorInterface} from '../route_origin_behavior.js';
/** @enum {number} */
const PageState = {
DISCONNECTED: 1,
DISCONNECTING: 2,
CONNECTING: 3,
CONNECTED: 4,
CONNECTION_FAILED: 5,
};
/**
* @constructor
* @extends {PolymerElement}
* @implements {RouteObserverBehaviorInterface}
* @implements {RouteOriginBehaviorInterface}
* @implements {I18nBehaviorInterface}
* @implements {WebUIListenerBehaviorInterface}
*/
const SettingsBluetoothDeviceDetailSubpageElementBase = mixinBehaviors(
[
RouteObserverBehavior,
RouteOriginBehavior,
I18nBehavior,
WebUIListenerBehavior,
],
PolymerElement);
/** @polymer */
class SettingsBluetoothDeviceDetailSubpageElement extends
SettingsBluetoothDeviceDetailSubpageElementBase {
static get is() {
return 'os-settings-bluetooth-device-detail-subpage';
}
static get template() {
return html`{__html_template__}`;
}
static get properties() {
return {
/**
* @type {!BluetoothSystemProperties}
*/
systemProperties: {
type: Object,
},
/**
* @private {?PairedBluetoothDeviceProperties}
*/
device_: {
type: Object,
observer: 'onDeviceChanged_',
},
/**
* Id of the currently paired device. This is set from the route query
* parameters.
* @private
*/
deviceId_: {
type: String,
value: '',
},
/** @private */
isDeviceConnected_: {
reflectToAttribute: true,
type: Boolean,
computed: 'computeIsDeviceConnected_(device_.*)',
},
/** @private */
shouldShowChangeDeviceNameDialog_: {
type: Boolean,
value: false,
},
/** @private {!PageState} */
pageState_: {
type: Object,
value: PageState.DISCONNECTED,
},
/** @protected */
shouldShowForgetDeviceDialog_: {
type: Boolean,
value: false,
},
};
}
static get observers() {
return [
'onSystemPropertiesOrDeviceIdChanged_(systemProperties.*, deviceId_)',
];
}
constructor() {
super();
/** RouteOriginBehaviorInterface override */
this.route_ = routes.BLUETOOTH_DEVICE_DETAIL;
}
/** @override */
ready() {
super.ready();
this.addEventListener(
'forget-bluetooth-device', this.forgetDeviceConfirmed_);
this.addFocusConfig(routes.POINTERS, '#changeMouseSettings');
this.addFocusConfig(routes.KEYBOARD, '#changeKeyboardSettings');
}
/**
* RouteObserverBehaviorInterface override
* @param {!Route} route
* @param {!Route=} opt_oldRoute
*/
currentRouteChanged(route, opt_oldRoute) {
super.currentRouteChanged(route, opt_oldRoute);
if (route !== this.route_) {
return;
}
this.deviceId_ = '';
this.pageState_ = PageState.DISCONNECTED;
this.device_ = null;
const queryParams = Router.getInstance().getQueryParameters();
const deviceId = queryParams.get('id') || '';
if (!deviceId) {
console.error('No id specified for page:' + route);
return;
}
this.deviceId_ = decodeURIComponent(deviceId);
recordBluetoothUiSurfaceMetrics(
BluetoothUiSurface.SETTINGS_DEVICE_DETAIL_SUBPAGE);
}
/** @private */
onSystemPropertiesOrDeviceIdChanged_() {
if (!this.systemProperties || !this.deviceId_) {
return;
}
this.device_ =
this.systemProperties.pairedDevices.find(
(device) => device.deviceProperties.id === this.deviceId_) ||
null;
if (this.device_ ||
Router.getInstance().currentRoute !== routes.BLUETOOTH_DEVICE_DETAIL) {
return;
}
// Special case where the device was turned off or becomes unavailable
// while user is vewing the page, return back to previous page.
this.deviceId_ = '';
Router.getInstance().navigateToPreviousRoute();
}
/**
* @return {boolean}
* @private
*/
computeIsDeviceConnected_() {
if (!this.device_) {
return false;
}
return this.device_.deviceProperties.connectionState ===
DeviceConnectionState.kConnected;
}
/**
* @return {string}
* @private
*/
getBluetoothStateIcon_() {
return this.isDeviceConnected_ ? 'os-settings:bluetooth-connected' :
'os-settings:bluetooth-disabled';
}
/**
* @return {string}
* @private
*/
getBluetoothConnectDisconnectBtnLabel_() {
return this.isDeviceConnected_ ? this.i18n('bluetoothDisconnect') :
this.i18n('bluetoothConnect');
}
/**
* @return {string}
* @private
*/
getBluetoothStateTextLabel_() {
if (this.pageState_ === PageState.CONNECTING) {
return this.i18n('bluetoothConnecting');
}
return this.pageState_ === PageState.CONNECTED ?
this.i18n('bluetoothDeviceDetailConnected') :
this.i18n('bluetoothDeviceDetailDisconnected');
}
/**
* @return {string}
* @private
*/
getDeviceName_() {
if (!this.device_) {
return '';
}
return getDeviceName(this.device_);
}
/**
* @return {boolean}
* @private
*/
shouldShowConnectDisconnectBtn_() {
if (!this.device_) {
return false;
}
return this.device_.deviceProperties.audioCapability ===
AudioOutputCapability.kCapableOfAudioOutput;
}
/**
* @return {boolean}
* @private
*/
shouldShowForgetBtn_() {
return !!this.device_;
}
/** @private */
onDeviceChanged_() {
if (!this.device_) {
return;
}
this.parentNode.pageTitle = getDeviceName(this.device_);
// Special case a where user is still on detail page and has
// tried to connect to device but failed. The current |pageState_|
// is CONNECTION_FAILED, but another device property not
// |connectionState| has changed.
if (this.pageState_ === PageState.CONNECTION_FAILED &&
this.device_.deviceProperties.connectionState ===
DeviceConnectionState.kNotConnected) {
return;
}
switch (this.device_.deviceProperties.connectionState) {
case DeviceConnectionState.kConnected:
this.pageState_ = PageState.CONNECTED;
break;
case DeviceConnectionState.kNotConnected:
this.pageState_ = PageState.DISCONNECTED;
break;
case DeviceConnectionState.kConnecting:
this.pageState_ = PageState.CONNECTING;
break;
default:
assertNotReached();
}
}
/**
* @return {boolean}
* @private
*/
shouldShowNonAudioOutputDeviceMessage_() {
if (!this.device_) {
return false;
}
return this.device_.deviceProperties.audioCapability !==
AudioOutputCapability.kCapableOfAudioOutput;
}
/**
* Message displayed for devices that are human interactive.
* @return {string}
* @private
*/
getNonAudioOutputDeviceMessage_() {
if (!this.device_) {
return '';
}
if (this.device_.deviceProperties.connectionState ===
DeviceConnectionState.kConnected) {
return this.i18n('bluetoothDeviceDetailHIDMessageConnected');
}
return this.i18n('bluetoothDeviceDetailHIDMessageDisconnected');
}
/** @private */
onChangeNameClick_() {
this.shouldShowChangeDeviceNameDialog_ = true;
}
/** @private */
onCloseChangeDeviceNameDialog_() {
this.shouldShowChangeDeviceNameDialog_ = false;
}
/**
* @return {string}
* @private
*/
getChangeDeviceNameBtnA11yLabel_() {
if (!this.device_) {
return '';
}
return this.i18n(
'bluetoothDeviceDetailChangeDeviceNameBtnA11yLabel',
this.getDeviceName_());
}
/**
* @return {string}
* @private
*/
getMultipleBatteryInfoA11yLabel_() {
let label = '';
const leftBudBatteryPercentage = getBatteryPercentage(
this.device_.deviceProperties, BatteryType.LEFT_BUD);
if (leftBudBatteryPercentage !== undefined) {
label = label +
this.i18n(
'bluetoothDeviceDetailLeftBudBatteryPercentageA11yLabel',
leftBudBatteryPercentage);
}
const caseBatteryPercentage =
getBatteryPercentage(this.device_.deviceProperties, BatteryType.CASE);
if (caseBatteryPercentage !== undefined) {
label = label +
this.i18n(
'bluetoothDeviceDetailCaseBatteryPercentageA11yLabel',
caseBatteryPercentage);
}
const rightBudBatteryPercentage = getBatteryPercentage(
this.device_.deviceProperties, BatteryType.RIGHT_BUD);
if (rightBudBatteryPercentage !== undefined) {
label = label +
this.i18n(
'bluetoothDeviceDetailRightBudBatteryPercentageA11yLabel',
rightBudBatteryPercentage);
}
return label;
}
/**
* @return {string}
* @private
*/
getBatteryInfoA11yLabel_() {
if (!this.device_) {
return '';
}
if (hasAnyDetailedBatteryInfo(this.device_.deviceProperties)) {
return this.getMultipleBatteryInfoA11yLabel_();
}
const batteryPercentage = getBatteryPercentage(
this.device_.deviceProperties, BatteryType.DEFAULT);
if (batteryPercentage === undefined) {
return '';
}
return this.i18n(
'bluetoothDeviceDetailBatteryPercentageA11yLabel', batteryPercentage);
}
/**
* @return {string}
* @private
*/
getDeviceStatusA11yLabel_() {
if (!this.device_) {
return '';
}
switch (this.pageState_) {
case PageState.CONNECTING:
return this.i18n(
'bluetoothDeviceDetailConnectingA11yLabel', this.getDeviceName_());
case PageState.CONNECTED:
return this.i18n(
'bluetoothDeviceDetailConnectedA11yLabel', this.getDeviceName_());
case PageState.CONNECTION_FAILED:
return this.i18n(
'bluetoothDeviceDetailConnectionFailureA11yLabel',
this.getDeviceName_());
case PageState.DISCONNECTED:
case PageState.DISCONNECTING:
return this.i18n(
'bluetoothDeviceDetailDisconnectedA11yLabel',
this.getDeviceName_());
default:
assertNotReached();
}
}
/**
* @return {boolean}
* @private
*/
shouldShowChangeMouseDeviceSettings_() {
if (!this.device_ || !this.isDeviceConnected_) {
return false;
}
return this.device_.deviceProperties.deviceType === DeviceType.kMouse ||
this.device_.deviceProperties.deviceType ===
DeviceType.kKeyboardMouseCombo;
}
/**
* @return {boolean}
* @private
*/
shouldShowChangeKeyboardDeviceSettings_() {
if (!this.device_ || !this.isDeviceConnected_) {
return false;
}
return this.device_.deviceProperties.deviceType === DeviceType.kKeyboard ||
this.device_.deviceProperties.deviceType ===
DeviceType.kKeyboardMouseCombo;
}
/**
* @return {boolean}
* @private
*/
shouldShowBlockedByPolicyIcon_() {
if (!this.device_) {
return false;
}
return this.device_.deviceProperties.isBlockedByPolicy;
}
/**
* @return {boolean}
* @private
*/
shouldShowBatteryInfo_() {
if (!this.device_ || this.pageState_ === PageState.CONNECTING ||
this.pageState_ === PageState.CONNECTION_FAILED) {
return false;
}
// Don't show the inline Battery Info if we are showing the True
// Wireless Images component.
if (this.shouldShowTrueWirelessImages_()) {
return false;
}
if (getBatteryPercentage(
this.device_.deviceProperties, BatteryType.DEFAULT) !== undefined) {
return true;
}
return hasAnyDetailedBatteryInfo(this.device_.deviceProperties);
}
/**
* @return {boolean}
* @private
*/
shouldShowTrueWirelessImages_() {
if (!this.device_) {
return false;
}
// The True Wireless Images component expects all True Wireless
// images and the default image to be displayable.
if (!hasDefaultImage(this.device_.deviceProperties) ||
!hasTrueWirelessImages(this.device_.deviceProperties)) {
return false;
}
// If the device is not connected, we don't need any battery info and can
// immediately return true.
if (!this.isDeviceConnected_) {
return true;
}
// Don't show True Wireless Images component if the device is connected and
// has no battery info to display.
return getBatteryPercentage(
this.device_.deviceProperties, BatteryType.DEFAULT) !==
undefined ||
hasAnyDetailedBatteryInfo(this.device_.deviceProperties);
}
/**
* @param {!Event} event
* @private
*/
onConnectDisconnectBtnClick_(event) {
event.stopPropagation();
if (this.pageState_ === PageState.DISCONNECTED ||
this.pageState_ === PageState.CONNECTION_FAILED) {
this.connectDevice_();
return;
}
this.disconnectDevice_();
}
/** @private */
connectDevice_() {
this.pageState_ = PageState.CONNECTING;
getBluetoothConfig().connect(this.deviceId_).then(response => {
this.handleConnectResult_(response.success);
});
}
/**
* @param {boolean} success
* @private
*/
handleConnectResult_(success) {
this.pageState_ =
success ? PageState.CONNECTED : PageState.CONNECTION_FAILED;
}
/** @private */
disconnectDevice_() {
this.pageState_ = PageState.DISCONNECTING;
getBluetoothConfig().disconnect(this.deviceId_).then(response => {
this.handleDisconnectResult_(response.success);
});
}
/**
* @param {boolean} success
* @private
*/
handleDisconnectResult_(success) {
if (success) {
this.pageState_ = PageState.DISCONNECTED;
}
}
/**
* @return {boolean}
* @private
*/
isConnectDisconnectBtnDisabled() {
return this.pageState_ === PageState.CONNECTING ||
this.pageState_ === PageState.DISCONNECTING;
}
/**
* @return {boolean}
* @private
*/
shouldShowErrorMessage_() {
return this.pageState_ === PageState.CONNECTION_FAILED;
}
/**
* @return {?PairedBluetoothDeviceProperties}
*/
getDeviceForTest() {
return this.device_;
}
/**
* @return {string}
*/
getDeviceIdForTest() {
return this.deviceId_;
}
/** @return {boolean} */
getIsDeviceConnectedForTest() {
return this.isDeviceConnected_;
}
/** @private */
onMouseRowClick_() {
Router.getInstance().navigateTo(routes.POINTERS);
}
/** @private */
onKeyboardRowClick_() {
Router.getInstance().navigateTo(routes.KEYBOARD);
}
/**
* @return {string}
* @private
*/
getForgetA11yLabel_() {
return this.i18n(
'bluetoothDeviceDetailForgetA11yLabel', this.getDeviceName_());
}
/** @private */
onForgetButtonClicked_() {
this.shouldShowForgetDeviceDialog_ = true;
}
/** @private */
onCloseForgetDeviceDialog_() {
this.shouldShowForgetDeviceDialog_ = false;
}
/** @private */
forgetDeviceConfirmed_() {
getBluetoothConfig().forget(this.deviceId_);
}
}
customElements.define(
SettingsBluetoothDeviceDetailSubpageElement.is,
SettingsBluetoothDeviceDetailSubpageElement);