// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_shared_vars.css.js';
import './shared_style.css.js';
import './np_list_object.js';
import './logging_tab.js';
import './log_object.js';
import './log_types.js';
import '//resources/ash/common/cr_elements/md_select.css.js';
import '//resources/ash/common/cr_elements/cros_color_overrides.css.js';
import 'chrome://resources/polymer/v3_0/iron-location/iron-location.js';
import 'chrome://resources/polymer/v3_0/iron-pages/iron-pages.js';
import {WebUiListenerMixin} from 'chrome://resources/ash/common/cr_elements/web_ui_listener_mixin.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {getTemplate} from './cross_device_internals.html.js';
import {NearbyLogsBrowserProxy} from './cross_device_logs_browser_proxy.js';
import type {LogTypesElement} from './log_types.js';
import {NearbyPrefsBrowserProxy} from './nearby_prefs_browser_proxy.js';
import {NearbyPresenceBrowserProxy} from './nearby_presence_browser_proxy.js';
import {NearbyUiTriggerBrowserProxy} from './nearby_ui_trigger_browser_proxy.js';
import type {LogMessage, LogProvider, PresenceDevice, SelectOption} from './types.js';
import {ActionValues, FeatureValues, Severity} from './types.js';
/**
* Converts log message to string format for saved download file.
*/
function logToSavedString(log: LogMessage): string {
// Convert to string value for |line.severity|.
let severity;
switch (log.severity) {
case Severity.INFO:
severity = 'INFO';
break;
case Severity.WARNING:
severity = 'WARNING';
break;
case Severity.ERROR:
severity = 'ERROR';
break;
case Severity.VERBOSE:
severity = 'VERBOSE';
break;
}
// Reduce the file path to just the file name for logging simplification.
const file = log.file.substring(log.file.lastIndexOf('/') + 1);
return `[${log.time} ${severity} ${file} (${log.line})] ${log.text}\n`;
}
const CrossDeviceInternalsElementBase = WebUiListenerMixin(PolymerElement);
class CrossDeviceInternalsElement extends CrossDeviceInternalsElementBase {
static get is() {
return 'cross-device-internals';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
npDiscoveredDevicesList_: {
type: Array,
value: () => [],
},
featuresList_: {
type: Array,
value: [
{name: 'Nearby Infra', value: FeatureValues.NEARBY_INFRA},
{name: 'Nearby Share', value: FeatureValues.NEARBY_SHARE},
{name: 'Fast Pair', value: FeatureValues.FAST_PAIR},
],
},
nearbyInfraActionList_: {
type: Array,
value: [
{name: 'NP: Start Scan', value: ActionValues.START_SCAN},
{name: 'NP: Stop Scan', value: ActionValues.STOP_SCAN},
{name: 'NP: Sync Credentials', value: ActionValues.SYNC_CREDENTIALS},
{name: 'NP: First time flow', value: ActionValues.FIRST_TIME_FLOW},
{
name: 'NP: Send Update Credentials Message',
value: ActionValues.SEND_UPDATE_CREDENTIALS_MESSAGE,
},
],
},
logLevelList_: {
type: Array,
value: [
{name: 'VERBOSE', value: Severity.VERBOSE},
{name: 'INFO', value: Severity.INFO},
{name: 'WARNING', value: Severity.WARNING},
{name: 'ERROR', value: Severity.ERROR},
],
},
nearbyShareActionList_: {
type: Array,
value: [
{name: 'Reset Nearby Share', value: ActionValues.RESET_NEARBY_SHARE},
],
},
fastPairActionList_: {
type: Array,
value: () => [],
},
actionsSelectList_: {
type: Array,
value: () => [],
},
logList_: {
type: Array,
value: () => [],
},
filteredLogList_: {
type: Array,
value: () => [],
},
currentSeverity: {
type: Severity,
value: Severity.VERBOSE,
},
currentLogTypes: {
type: FeatureValues,
value: [
FeatureValues.NEARBY_SHARE,
FeatureValues.NEARBY_INFRA,
FeatureValues.FAST_PAIR,
],
},
};
}
private npDiscoveredDevicesList_: PresenceDevice[];
private featuresList_: SelectOption[];
private nearbyInfraActionList_: SelectOption[];
private nearbyShareActionList_: SelectOption[];
private fastPairActionList_: SelectOption[];
private actionsSelectList_: SelectOption[];
private logList_: LogMessage[];
private filteredLogList_: LogMessage[];
private currentFilter_: string;
private currentSeverity: Severity;
private logLevelList_: SelectOption[];
private logProvider_: LogProvider;
private currentLogTypes: FeatureValues[];
private nearbyPresenceBrowserProxy_: NearbyPresenceBrowserProxy =
NearbyPresenceBrowserProxy.getInstance();
private prefsBrowserProxy_: NearbyPrefsBrowserProxy =
NearbyPrefsBrowserProxy.getInstance();
private nearbyUITriggerBrowserProxy_: NearbyUiTriggerBrowserProxy =
NearbyUiTriggerBrowserProxy.getInstance();
/**
* When the page is initialized, notify the C++ layer and load in the
* contents of its log buffer. Initialize WebUI Listeners.
*/
override connectedCallback() {
super.connectedCallback();
this.nearbyPresenceBrowserProxy_.initialize();
this.nearbyUITriggerBrowserProxy_.initialize();
this.addWebUiListener(
'presence-device-found',
(device: PresenceDevice) => this.onPresenceDeviceFound_(device));
this.addWebUiListener(
'presence-device-changed',
(device: PresenceDevice) => this.onPresenceDeviceChanged_(device));
this.addWebUiListener(
'presence-device-lost',
(device: PresenceDevice) => this.onPresenceDeviceLost_(device));
this.set('actionsSelectList_', this.nearbyInfraActionList_);
this.logProvider_ = {
messageAddedEventName: 'log-message-added',
bufferClearedEventName: 'log-buffer-cleared',
logFilePrefix: 'cross_device_logs_',
getLogMessages: () =>
NearbyLogsBrowserProxy.getInstance().getLogMessages(),
};
this.addWebUiListener(
this.logProvider_.messageAddedEventName,
(log: LogMessage) => this.onLogMessageAdded_(log));
this.addWebUiListener(
this.logProvider_.bufferClearedEventName,
() => this.onWebUiLogBufferCleared_());
this.logProvider_.getLogMessages().then(
(logs: LogMessage[]) => this.onGetLogMessages_(logs));
}
private updateActionsSelect_() {
const actionGroup: HTMLSelectElement|null =
this.shadowRoot!.querySelector('#actionGroup');
if (actionGroup) {
switch (Number(actionGroup.value)) {
case FeatureValues.NEARBY_INFRA:
this.set('actionsSelectList_', this.nearbyInfraActionList_);
break;
case FeatureValues.NEARBY_SHARE:
this.set('actionsSelectList_', this.nearbyShareActionList_);
break;
case FeatureValues.FAST_PAIR:
this.set('actionsSelectList_', this.fastPairActionList_);
break;
}
}
}
private performAction_() {
const actionSelect: HTMLSelectElement|null =
this.shadowRoot!.querySelector('#actionSelect');
if (actionSelect) {
switch (Number(actionSelect.value)) {
case ActionValues.START_SCAN:
this.nearbyPresenceBrowserProxy_.sendStartScan();
break;
case ActionValues.STOP_SCAN:
this.nearbyPresenceBrowserProxy_.sendStopScan();
break;
case ActionValues.SYNC_CREDENTIALS:
this.nearbyPresenceBrowserProxy_.sendSyncCredentials();
break;
case ActionValues.FIRST_TIME_FLOW:
this.nearbyPresenceBrowserProxy_.sendFirstTimeFlow();
break;
case ActionValues.RESET_NEARBY_SHARE:
this.prefsBrowserProxy_.clearNearbyPrefs();
break;
case ActionValues.SEND_UPDATE_CREDENTIALS_MESSAGE:
this.nearbyPresenceBrowserProxy_
.sendUpdateCredentialsPushNotificationMessage();
break;
case ActionValues.SHOW_RECEIVED_NOTIFICATION:
this.nearbyUITriggerBrowserProxy_
.showNearbyShareReceivedNotification();
break;
default:
break;
}
}
}
private onPresenceDeviceFound_(device: PresenceDevice): void {
const type = device['type'];
const endpointId = device['endpoint_id'];
const actions = device['actions'];
// If there is not a device with this endpoint_id currently in the devices
// list, add it.
if (!this.npDiscoveredDevicesList_.find(
listDevice => listDevice.endpoint_id === endpointId)) {
this.unshift('npDiscoveredDevicesList_', {
'connectable': true,
'type': type,
'endpoint_id': endpointId,
'actions': actions,
});
}
}
// TODO(b/277820435): Add and update device name for devices that have names
// included.
private onPresenceDeviceChanged_(device: PresenceDevice): void {
const type = device['type'];
const endpointId = device['endpoint_id'];
const actions = device['actions'];
const index = this.npDiscoveredDevicesList_.findIndex(
listDevice => listDevice.endpoint_id === endpointId);
// If a device was changed but we don't have a record of it being found,
// add it to the array like performActiononPresenceDeviceFound__().
if (index === -1) {
this.unshift('npDiscoveredDevicesList_', {
'connectable': true,
'type': type,
'endpoint_id': endpointId,
'actions': actions,
});
return;
}
this.npDiscoveredDevicesList_[index] = {
'connectable': true,
'type': type,
'endpoint_id': endpointId,
'actions': actions,
};
}
private onPresenceDeviceLost_(device: PresenceDevice): void {
const type = device['type'];
const endpointId = device['endpoint_id'];
const actions = device['actions'];
const index = this.npDiscoveredDevicesList_.findIndex(
listDevice => listDevice.endpoint_id === endpointId);
// The device was not found in the list.
if (index === -1) {
return;
}
this.npDiscoveredDevicesList_[index] = {
'connectable': false,
'type': type,
'endpoint_id': endpointId,
'actions': actions,
};
}
/**
* Clears javascript logs displayed, but c++ log buffer remains.
*/
private onClearLogsButtonClicked_(): void {
this.clearLogBuffer_();
}
/**
* Saves and downloads all javascript logs.
*/
private onSaveUnfilteredLogsButtonClicked_(): void {
this.onSaveLogsButtonClicked_(false);
}
/**
* Saves and downloads javascript logs that currently appear on the page.
*/
private onSaveFilteredLogsButtonClicked_(): void {
this.onSaveLogsButtonClicked_(true);
}
/**
* Saves and downloads javascript logs.
*/
private onSaveLogsButtonClicked_(filtered: boolean): void {
let blob;
if (filtered) {
blob = new Blob(
this.filteredLogList_.map(logToSavedString),
{type: 'text/plain;charset=utf-8'});
} else {
blob = new Blob(
this.logList_.map(logToSavedString),
{type: 'text/plain;charset=utf-8'});
}
const url = URL.createObjectURL(blob);
const anchorElement = document.createElement('a');
anchorElement.href = url;
anchorElement.download =
this.logProvider_.logFilePrefix + new Date().toJSON() + '.txt';
document.body.appendChild(anchorElement);
anchorElement.click();
window.setTimeout(function() {
document.body.removeChild(anchorElement);
window.URL.revokeObjectURL(url);
}, 0);
}
/**
* Adds a log message to the javascript log list displayed. Called from the
* C++ WebUI handler when a log message is added to the log buffer.
*/
private onLogMessageAdded_(log: LogMessage): void {
this.push('logList_', log);
if ((log.text.match(this.currentFilter_) ||
log.file.match(this.currentFilter_)) &&
log.severity >= this.currentSeverity &&
this.currentLogTypes.includes(log.feature)) {
this.push('filteredLogList_', log);
}
}
private addLogFilter_(): void {
const logLevelSelector: HTMLSelectElement|null =
this.shadowRoot!.querySelector('#logLevelSelector');
if (logLevelSelector) {
switch (Number(logLevelSelector.value)) {
case Severity.VERBOSE:
this.set(
'filteredLogList_',
this.logList_.filter((log) => log.severity >= Severity.VERBOSE));
this.currentSeverity = Severity.VERBOSE;
break;
case Severity.INFO:
this.set(
'filteredLogList_',
this.logList_.filter((log) => log.severity >= Severity.INFO));
this.currentSeverity = Severity.INFO;
break;
case Severity.WARNING:
this.set(
'filteredLogList_',
this.logList_.filter((log) => log.severity >= Severity.WARNING));
this.currentSeverity = Severity.WARNING;
break;
case Severity.ERROR:
this.set(
'filteredLogList_',
this.logList_.filter((log) => log.severity >= Severity.ERROR));
this.currentSeverity = Severity.ERROR;
break;
}
}
const logType: LogTypesElement|null =
this.shadowRoot!.querySelector('#logType');
if (logType) {
this.set(
'currentLogTypes',
logType.currentLogTypes,
);
}
this.set(
'filteredLogList_',
this.filteredLogList_.filter(
(log: LogMessage) => this.currentLogTypes.includes(log.feature)));
const logSearch: HTMLSelectElement|null =
this.shadowRoot!.querySelector('#logSearch');
if (logSearch) {
this.currentFilter_ = logSearch.value;
this.set(
'filteredLogList_',
this.filteredLogList_.filter(
(log: LogMessage) =>
(log.text.match(this.currentFilter_) ||
log.file.match(this.currentFilter_))));
}
}
/**
* Called in response to WebUI handler clearing log buffer.
*/
private onWebUiLogBufferCleared_(): void {
this.clearLogBuffer_();
}
/**
* Parses an array of log messages and adds to the javascript list sent in
* from the initial page load.
*/
private onGetLogMessages_(logs: LogMessage[]): void {
this.logList_ = logs.concat(this.logList_);
this.filteredLogList_ = logs.slice();
}
/**
* Clears the javascript log buffer.
*/
private clearLogBuffer_(): void {
this.logList_ = [];
this.filteredLogList_ = [];
}
}
customElements.define(
CrossDeviceInternalsElement.is, CrossDeviceInternalsElement);