// 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.
import './diagnostics_shared.css.js';
import './input_card.js';
import './keyboard_tester.js';
import './touchscreen_tester.js';
import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {assert} from 'chrome://resources/js/assert.js';
import {PolymerElementProperties} from 'chrome://resources/polymer/v3_0/polymer/interfaces.js';
import {afterNextRender, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {DiagnosticsBrowserProxy, DiagnosticsBrowserProxyImpl} from './diagnostics_browser_proxy.js';
import {ConnectionType, KeyboardInfo} from './input.mojom-webui.js';
import {InputCardElement} from './input_card.js';
import {ConnectedDevicesObserverReceiver, InputDataProviderInterface, InternalDisplayPowerStateObserverReceiver, LidStateObserverReceiver, TabletModeObserverReceiver, TouchDeviceInfo, TouchDeviceType} from './input_data_provider.mojom-webui.js';
import {getTemplate} from './input_list.html.js';
import {KeyboardTesterElement} from './keyboard_tester.js';
import {getInputDataProvider} from './mojo_interface_provider.js';
import {TouchpadTesterElement} from './touchpad_tester.js';
import {TouchscreenTesterElement} from './touchscreen_tester.js';
/**
* @fileoverview
* 'input-list' is responsible for displaying keyboard, touchpad, and
* touchscreen cards.
*/
const InputListElementBase = I18nMixin(PolymerElement);
export interface HostDeviceStatus {
isLidOpen: boolean;
isTabletMode: boolean;
}
export class InputListElement extends InputListElementBase {
static get is(): string {
return 'input-list';
}
static get template(): HTMLTemplateElement {
return getTemplate();
}
static get properties(): PolymerElementProperties {
return {
keyboards: {
type: Array,
value: () => [],
},
touchpads: {
type: Array,
value: () => [],
},
touchscreens: {
type: Array,
value: () => [],
},
showTouchpads: {
type: Boolean,
computed: 'computeShowTouchpads(touchpads.length)',
},
showTouchscreens: {
type: Boolean,
computed: 'computeShowTouchscreens(touchscreens.length)',
},
touchscreenIdUnderTesting: {
type: Number,
value: -1,
notify: true,
},
hostDeviceStatus: {
type: Object,
},
};
}
protected showTouchpads: boolean;
protected showTouchscreens: boolean;
// The evdev id of touchscreen under testing.
protected touchscreenIdUnderTesting: number = -1;
protected hostDeviceStatus:
HostDeviceStatus = {isLidOpen: false, isTabletMode: false};
private keyboards: KeyboardInfo[];
private touchpads: TouchDeviceInfo[];
private touchscreens: TouchDeviceInfo[];
private connectedDevicesObserverReceiver: ConnectedDevicesObserverReceiver|
null = null;
private internalDisplayPowerStateObserverReceiver:
InternalDisplayPowerStateObserverReceiver|null = null;
private tabletModeReceiver: TabletModeObserverReceiver|null = null;
private lidStateReceiver: LidStateObserverReceiver|null = null;
private keyboardTester: KeyboardTesterElement;
private touchscreenTester: TouchscreenTesterElement|null = null;
private touchpadTester: TouchpadTesterElement|null = null;
private browserProxy: DiagnosticsBrowserProxy =
DiagnosticsBrowserProxyImpl.getInstance();
private inputDataProvider: InputDataProviderInterface =
getInputDataProvider();
private computeShowTouchpads(numTouchpads: number): boolean {
return numTouchpads > 0 && loadTimeData.getBoolean('isTouchpadEnabled');
}
private computeShowTouchscreens(numTouchscreens: number): boolean {
return numTouchscreens > 0 &&
loadTimeData.getBoolean('isTouchscreenEnabled');
}
constructor() {
super();
this.browserProxy.initialize();
this.loadInitialDevices().then(() => {
this.handleKeyboardTesterDirectOpen();
});
this.observeConnectedDevices();
this.observeInternalDisplayPowerState();
this.observeLidState();
this.observeTabletMode();
}
override connectedCallback(): void {
super.connectedCallback();
const keyboardTester = this.shadowRoot!.querySelector('keyboard-tester');
assert(keyboardTester);
this.keyboardTester = keyboardTester;
}
private loadInitialDevices(): Promise<void> {
return this.inputDataProvider.getConnectedDevices().then((devices) => {
this.keyboards = devices.keyboards;
this.touchpads = devices.touchDevices.filter(
(device: TouchDeviceInfo) =>
device.type === TouchDeviceType.kPointer);
this.touchscreens = devices.touchDevices.filter(
(device: TouchDeviceInfo) => device.type === TouchDeviceType.kDirect);
});
}
private observeConnectedDevices(): void {
this.connectedDevicesObserverReceiver =
new ConnectedDevicesObserverReceiver(this);
this.inputDataProvider.observeConnectedDevices(
this.connectedDevicesObserverReceiver.$.bindNewPipeAndPassRemote());
}
private observeInternalDisplayPowerState(): void {
this.internalDisplayPowerStateObserverReceiver =
new InternalDisplayPowerStateObserverReceiver(this);
this.inputDataProvider.observeInternalDisplayPowerState(
this.internalDisplayPowerStateObserverReceiver.$
.bindNewPipeAndPassRemote());
}
private observeLidState(): void {
this.lidStateReceiver = new LidStateObserverReceiver(this);
this.inputDataProvider
.observeLidState(this.lidStateReceiver.$.bindNewPipeAndPassRemote())
.then(({isLidOpen}: {isLidOpen: boolean}) => {
this.onLidStateChanged(isLidOpen);
});
}
private observeTabletMode(): void {
this.tabletModeReceiver = new TabletModeObserverReceiver(this);
this.inputDataProvider
.observeTabletMode(this.tabletModeReceiver.$.bindNewPipeAndPassRemote())
.then(({isTabletMode}: {isTabletMode: boolean}) => {
this.onTabletModeChanged(isTabletMode);
});
}
/**
* Implements
* InternalDisplayPowerStateObserver.OnInternalDisplayPowerStateChanged.
* @param isDisplayOn Just applied value of whether the display power is on.
*/
onInternalDisplayPowerStateChanged(isDisplayOn: boolean): void {
// Find the internal touchscreen.
const index = this.touchscreens.findIndex(
(device: TouchDeviceInfo) =>
device.connectionType === ConnectionType.kInternal);
if (index != -1) {
// Copy object to enforce dom to re-render.
const internalTouchscreen = {...this.touchscreens[index]};
internalTouchscreen.testable = isDisplayOn;
this.splice('touchscreens', index, 1, internalTouchscreen);
// If the internal display becomes untestable, and it is currently under
// testing, close the touchscreen tester.
if (!isDisplayOn &&
internalTouchscreen.id === this.touchscreenIdUnderTesting) {
assert(this.touchscreenTester);
this.touchscreenTester.closeTester();
}
}
}
/**
* Implements ConnectedDevicesObserver.OnKeyboardConnected.
*/
onKeyboardConnected(newKeyboard: KeyboardInfo): void {
this.push('keyboards', newKeyboard);
}
/**
* Removes the device with the given evdev ID from one of the device list
* properties.
* @param path the property's path
*/
private removeDeviceById(
path: 'keyboards'|'touchpads'|'touchscreens', id: number): void {
const index = this.get(path).findIndex(
(device: KeyboardInfo|TouchDeviceInfo) => device.id === id);
if (index !== -1) {
this.splice(path, index, 1);
}
}
private showDeviceDisconnectedToast(): void {
this.dispatchEvent(new CustomEvent('show-toast', {
composed: true,
bubbles: true,
detail: {message: loadTimeData.getString('deviceDisconnected')},
}));
}
/**
* Implements ConnectedDevicesObserver.OnKeyboardDisconnected.
*/
onKeyboardDisconnected(id: number): void {
this.removeDeviceById('keyboards', id);
if (this.keyboards.length === 0 && this.keyboardTester?.isOpen()) {
// When no keyboards are connected, the <diagnostics-app> component hides
// the input page. If that happens while a <cr-dialog> is open, the rest
// of the app remains unresponsive due to the dialog's native logic
// blocking interaction with other elements. To prevent this we have to
// explicitly close the dialog when this happens.
this.keyboardTester.close();
this.showDeviceDisconnectedToast();
}
}
/**
* Implements ConnectedDevicesObserver.OnTouchDeviceConnected.
*/
onTouchDeviceConnected(newTouchDevice: TouchDeviceInfo): void {
if (newTouchDevice.type === TouchDeviceType.kPointer) {
this.push('touchpads', newTouchDevice);
} else {
this.push('touchscreens', newTouchDevice);
}
}
/**
* Implements ConnectedDevicesObserver.OnTouchDeviceDisconnected.
*/
onTouchDeviceDisconnected(id: number): void {
this.removeDeviceById('touchpads', id);
this.removeDeviceById('touchscreens', id);
// If the touchscreen under testing is disconnected, close the touchscreen
// tester.
if (id === this.touchscreenIdUnderTesting) {
assert(this.touchscreenTester);
this.touchscreenTester.closeTester();
}
}
private handleKeyboardTestButtonClick(e: CustomEvent): void {
const keyboard: KeyboardInfo|undefined = this.keyboards.find(
(keyboard: KeyboardInfo) => keyboard.id === e.detail.evdevId);
assert(keyboard);
this.keyboardTester.keyboard = keyboard;
this.keyboardTester.show();
}
/**
* Show the keyboard tester directly if `showDefaultKeyboardTester` is present
* in the query string.
*/
private handleKeyboardTesterDirectOpen(): void {
const params = new URLSearchParams(window.location.search);
if (params.has('showDefaultKeyboardTester') && this.keyboards.length > 0 &&
!this.keyboardTester?.isOpen()) {
this.keyboardTester.keyboard = this.keyboards[0];
this.keyboardTester.show();
}
}
/**
* Shows touchpad-tester interface when input-card "test" button for specific
* device is clicked.
*/
protected handleTouchpadTestButtonClick(e: CustomEvent): void {
this.touchpadTester =
this.shadowRoot!.querySelector(TouchpadTesterElement.is);
assert(this.touchpadTester);
const touchpad: TouchDeviceInfo|undefined = this.touchpads.find(
(touchpad: TouchDeviceInfo) => touchpad.id === e.detail.evdevId);
assert(touchpad);
this.touchpadTester.show(touchpad);
}
/**
* Handles when the touchscreen Test button is clicked.
*/
private handleTouchscreenTestButtonClick(e: CustomEvent): void {
this.touchscreenTester =
this.shadowRoot!.querySelector('touchscreen-tester');
assert(this.touchscreenTester);
this.touchscreenIdUnderTesting = e.detail.evdevId;
this.touchscreenTester.showTester(e.detail.evdevId);
}
/**
* 'navigation-view-panel' is responsible for calling this function when
* the active page changes.
*/
onNavigationPageChanged({isActive}: {isActive: boolean}): void {
if (isActive) {
// Focus the first visible card title. If no cards are present,
// fallback to focusing the element's main container.
afterNextRender(this, () => {
if (this.keyboards) {
const keyboard: InputCardElement|null =
this.shadowRoot!.querySelector('#keyboardInputCard');
assert(keyboard);
const keyboardTitle: HTMLDivElement|null =
keyboard.querySelector('#keyboardTitle');
assert(keyboardTitle);
keyboardTitle.focus();
} else {
const inputListContainer: HTMLDivElement|null =
this.shadowRoot!.querySelector('#inputListContainer');
assert(inputListContainer);
inputListContainer.focus();
}
});
// TODO(ashleydp): Remove when a call can be made at a higher component
// to avoid duplicate code in all navigatable pages.
this.browserProxy.recordNavigation('input');
}
}
onHostDeviceStatusChanged(): void {
// If the keyboard tester isn't open or we aren't testing an internal
// keyboard, do nothing.
if (!this.keyboardTester.isOpen() ||
this.keyboardTester.keyboard.connectionType !=
ConnectionType.kInternal) {
return;
}
// Keyboard tester remains open if the lid is open and we are not in tablet
// mode.
if (this.hostDeviceStatus.isLidOpen &&
!this.hostDeviceStatus.isTabletMode) {
return;
}
this.keyboardTester.close();
this.dispatchEvent(new CustomEvent('show-toast', {
composed: true,
bubbles: true,
detail: {message: this.getKeyboardTesterClosedToastString()},
}));
}
getKeyboardTesterClosedToastString(): string {
if (!this.hostDeviceStatus.isLidOpen) {
return loadTimeData.getString('inputKeyboardTesterClosedToastLidClosed');
}
if (this.hostDeviceStatus.isTabletMode) {
return loadTimeData.getString('inputKeyboardTesterClosedToastTabletMode');
}
return loadTimeData.getString('deviceDisconnected');
}
/**
* Implements TabletModeObserver.OnTabletModeChanged.
* @param isTabletMode Is current display on tablet mode.
*/
onTabletModeChanged(isTabletMode: boolean): void {
this.hostDeviceStatus = {
...this.hostDeviceStatus,
isTabletMode: isTabletMode,
};
this.onHostDeviceStatusChanged();
}
onLidStateChanged(isLidOpen: boolean): void {
this.hostDeviceStatus = {
...this.hostDeviceStatus,
isLidOpen: isLidOpen,
};
this.onHostDeviceStatusChanged();
}
}
declare global {
interface HTMLElementTagNameMap {
'input-list': InputListElement;
}
}
customElements.define(InputListElement.is, InputListElement);