blob: 506b36d4df76266618710da0d4246fc8f542dfdd [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ChangeDetectorRef,
Component,
EventEmitter,
Inject,
Input,
NgZone,
Output,
ViewEncapsulation,
} from '@angular/core';
import {MatDialog} from '@angular/material/dialog';
import {MatSelectChange} from '@angular/material/select';
import {
assertDefined,
assertTrue,
assertUnreachable,
} from 'common/assert_utils';
import {FunctionUtils} from 'common/function_utils';
import {Store} from 'common/store/store';
import {UserNotifier} from 'common/user_notifier';
import {Analytics} from 'logging/analytics';
import {ProgressListener} from 'messaging/progress_listener';
import {ProxyTraceTimeout} from 'messaging/user_warnings';
import {
NoTraceTargetsSelected,
WinscopeEvent,
WinscopeEventType,
} from 'messaging/winscope_event';
import {
EmitEvent,
WinscopeEventEmitter,
} from 'messaging/winscope_event_emitter';
import {WinscopeEventListener} from 'messaging/winscope_event_listener';
import {
AdbDeviceConnection,
AdbDeviceState,
} from 'trace_collection/adb/adb_device_connection';
import {AdbConnectionType} from 'trace_collection/adb_connection_type';
import {AdbFiles, RequestedTraceTypes} from 'trace_collection/adb_files';
import {ConnectionState} from 'trace_collection/connection_state';
import {ConnectionStateListener} from 'trace_collection/connection_state_listener';
import {TraceCollectionController} from 'trace_collection/controller/trace_collection_controller';
import {
CheckboxConfiguration,
makeDefaultDumpConfigMap,
makeDefaultTraceConfigMap,
makeScreenRecordingSelectionConfigs,
SelectionConfiguration,
TraceConfigurationMap,
updateConfigsFromStore,
} from 'trace_collection/ui/ui_trace_configuration';
import {UiTraceTarget} from 'trace_collection/ui/ui_trace_target';
import {UserRequest, UserRequestConfig} from 'trace_collection/user_request';
import {LoadProgressComponent} from './load_progress_component';
import {
WarningDialogComponent,
WarningDialogData,
WarningDialogResult,
} from './warning_dialog_component';
@Component({
selector: 'collect-traces',
template: `
<mat-card class="collect-card">
<mat-card-title class="title">Collect Traces</mat-card-title>
<mat-card-content *ngIf="controller" class="collect-card-content">
<mat-form-field class="connection-type">
<mat-label>Select connection type</mat-label>
<mat-select
[value]="getConnectionType()"
(selectionChange)="onConnectionChange($event)"
[disabled]="disableTraceSection()">
<mat-option [value]="AdbConnectionType.WINSCOPE_PROXY">
<span>{{AdbConnectionType.WINSCOPE_PROXY}}</span>
</mat-option>
<mat-option [value]="AdbConnectionType.WDP">
<span>{{AdbConnectionType.WDP}}</span>
</mat-option>
</mat-select>
</mat-form-field>
<button
mat-icon-button
class="refresh-connection"
(click)="onRetryConnection()"
matTooltip="Refresh connection"><mat-icon>refresh</mat-icon></button>
<ng-container *ngIf="!adbSuccess()">
<winscope-proxy-setup
*ngIf="getConnectionType() === AdbConnectionType.WINSCOPE_PROXY"
[state]="state"
(retryConnection)="onRetryConnection($event)"></winscope-proxy-setup>
<wdp-setup
*ngIf="getConnectionType() === AdbConnectionType.WDP"
[state]="state"
(retryConnection)="onRetryConnection()"></wdp-setup>
</ng-container>
<div *ngIf="showAllDevices()" class="devices-connecting">
<div
*ngIf="controller.getDevices().length === 0"
class="no-device-detected">
<p class="mat-body-3 icon">
<mat-icon inline fontIcon="phonelink_erase"></mat-icon>
</p>
<p class="mat-body-1">No devices detected</p>
</div>
<div
*ngIf="controller.getDevices().length > 0"
class="device-selection">
<p class="mat-body-1 instruction">Select a device:</p>
<mat-list>
<mat-list-item
*ngFor="let device of controller.getDevices()"
[disabled]="device.state === ${AdbDeviceState.OFFLINE}"
(click)="onDeviceClick(device)"
class="available-device">
<mat-icon matListIcon>
{{ getDeviceStateIcon(device.state) }}
</mat-icon>
<p matLine>
{{ getDeviceName(device) }}
</p>
<mat-icon
*ngIf="showTryAuthorizeButton(device)"
class="material-symbols-outlined authorize-btn"
matTooltip="Authorize device"
(click)="device.tryAuthorize()">lock_open</mat-icon>
</mat-list-item>
</mat-list>
</div>
</div>
<div
*ngIf="showTraceCollectionConfig()"
class="trace-collection-config">
<mat-list>
<mat-list-item class="selected-device">
<mat-icon matListIcon>smartphone</mat-icon>
<p matLine>
{{ getSelectedDevice()}}
</p>
<div class="device-actions">
<button
color="primary"
class="change-btn"
mat-stroked-button
(click)="onChangeDeviceButton()"
[disabled]="isTracingOrLoading()">
Change device
</button>
<button
color="primary"
class="fetch-btn"
mat-stroked-button
(click)="fetchExistingTraces()"
[disabled]="isTracingOrLoading()">
Fetch traces from last session
</button>
</div>
</mat-list-item>
</mat-list>
<mat-tab-group [selectedIndex]="targetTabIndex" class="target-tabs">
<mat-tab
label="Trace"
[disabled]="disableTraceSection()">
<div class="tabbed-section">
<div
class="trace-section"
*ngIf="state === ${ConnectionState.IDLE}">
<trace-config
title="Trace targets"
[traceConfig]="traceConfig"
[storage]="storage"
[traceConfigStoreKey]="storeKeyPrefixTraceConfig"
(traceConfigChange)="onTraceConfigChange($event)"></trace-config>
<div class="start-btn">
<button
color="primary"
mat-raised-button
(click)="startTracing()">Start trace</button>
</div>
</div>
<div *ngIf="isTracingOrLoading()" class="tracing-progress">
<load-progress
[icon]="progressIcon"
[message]="progressMessage"
[progressPercentage]="progressPercentage">
</load-progress>
<div class="end-btn" *ngIf="isTracing()">
<button
color="primary"
mat-raised-button
[disabled]="state !== ${ConnectionState.TRACING}"
(click)="endTrace()">
End trace
</button>
</div>
</div>
</div>
</mat-tab>
<mat-tab
label="Dump"
[disabled]="isTracingOrLoading()">
<div class="tabbed-section">
<div
class="dump-section"
*ngIf="state === ${ConnectionState.IDLE} && !refreshDumps">
<trace-config
title="Dump targets"
[traceConfig]="dumpConfig"
[storage]="storage"
[traceConfigStoreKey]="storeKeyPrefixDumpConfig"
(traceConfigChange)="onDumpConfigChange($event)"></trace-config>
<div class="dump-btn" *ngIf="!refreshDumps">
<button
color="primary"
mat-raised-button
(click)="dumpState()">Dump state</button>
</div>
</div>
<load-progress
class="dumping-state"
*ngIf="isDumpingState()"
[progressPercentage]="progressPercentage"
[message]="progressMessage">
</load-progress>
</div>
</mat-tab>
</mat-tab-group>
</div>
<div *ngIf="state === ${ConnectionState.ERROR}" class="unknown-error">
<p class="error-wrapper mat-body-1">
<mat-icon class="error-icon">error</mat-icon>
Error:
</p>
<pre> {{ errorText }} </pre>
<button
color="primary"
class="retry-btn"
mat-raised-button
(click)="onRetryButton()">Retry</button>
</div>
</mat-card-content>
</mat-card>
`,
styles: [
`
.change-btn,
.retry-btn,
.fetch-btn {
margin-left: 5px;
}
.fetch-btn {
margin-top: 5px;
}
.selected-device {
height: fit-content !important;
}
.mat-card.collect-card {
display: flex;
}
.collect-card {
height: 100%;
flex-direction: column;
overflow: auto;
margin: 10px;
}
.collect-card-content {
overflow: auto;
}
.selection {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
}
.trace-collection-config,
.trace-section,
.dump-section,
.tracing-progress,
trace-config {
display: flex;
flex-direction: column;
gap: 10px;
}
.trace-section,
.dump-section,
.tracing-progress {
height: 100%;
}
.winscope-proxy-setup-tab,
.web-tab,
.start-btn,
.dump-btn,
.end-btn {
align-self: flex-start;
}
.start-btn,
.dump-btn,
.end-btn {
margin: auto 0 0 0;
padding: 1rem 0 0 0;
}
.error-wrapper {
display: flex;
flex-direction: row;
align-items: center;
}
.error-icon {
margin-right: 5px;
}
.available-device {
cursor: pointer;
}
.no-device-detected {
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
align-items: center;
height: 100%;
}
.no-device-detected p,
.device-selection p.instruction {
padding-top: 1rem;
opacity: 0.6;
font-size: 1.2rem;
}
.no-device-detected .icon {
font-size: 3rem;
margin: 0 0 0.2rem 0;
}
mat-card-content {
flex-grow: 1;
}
mat-tab-body {
padding: 1rem;
}
.loading-info {
opacity: 0.8;
padding: 1rem 0;
}
.target-tabs {
flex-grow: 1;
}
.target-tabs .mat-tab-body-wrapper {
flex-grow: 1;
}
.tabbed-section {
height: 100%;
}
.progress-desc {
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-content: center;
align-items: center;
}
.progress-desc > * {
max-width: 250px;
}
load-progress {
height: 100%;
}
`,
],
encapsulation: ViewEncapsulation.None,
})
export class CollectTracesComponent
implements
ProgressListener,
WinscopeEventListener,
WinscopeEventEmitter,
ConnectionStateListener
{
objectKeys = Object.keys;
AdbConnectionType = AdbConnectionType;
isExternalOperationInProgress = false;
progressMessage = 'Fetching...';
progressIcon = 'sync';
progressPercentage: number | undefined;
lastUiProgressUpdateTimeMs?: number;
refreshDumps = false;
targetTabIndex = 0;
traceConfig: TraceConfigurationMap;
dumpConfig: TraceConfigurationMap;
requestedTraceTypes: RequestedTraceTypes[] = [];
controller: TraceCollectionController | undefined;
state = ConnectionState.CONNECTING;
errorText = '';
readonly storeKeyPrefixTraceConfig = 'TraceSettings.';
readonly storeKeyPrefixDumpConfig = 'DumpSettings.';
private readonly storeKeyImeWarning = 'doNotShowImeWarningDialog';
private readonly storeKeyLastDevice = 'adb.lastDevice';
private readonly storeKeyAdbConnectionType = 'adbConnectionType';
private selectedDevice: AdbDeviceConnection | undefined;
private emitEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
private readonly notConnected = [
ConnectionState.CONNECTING,
ConnectionState.NOT_FOUND,
ConnectionState.UNAUTH,
ConnectionState.INVALID_VERSION,
];
private readonly tracingSessionStates = [
ConnectionState.STARTING_TRACE,
ConnectionState.TRACING,
ConnectionState.ENDING_TRACE,
ConnectionState.DUMPING_STATE,
];
@Input() storage: Store | undefined;
@Output() readonly filesCollected = new EventEmitter<AdbFiles>();
constructor(
@Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef,
@Inject(MatDialog) private dialog: MatDialog,
@Inject(NgZone) private ngZone: NgZone,
) {
this.traceConfig = makeDefaultTraceConfigMap();
this.dumpConfig = makeDefaultDumpConfigMap();
}
async ngOnInit() {
const adbConnectionType = this.storage?.get(this.storeKeyAdbConnectionType);
if (adbConnectionType !== undefined) {
await this.changeHostConnection(adbConnectionType);
} else {
await this.changeHostConnection(AdbConnectionType.WINSCOPE_PROXY);
}
}
getConnectionType() {
return this.controller?.getConnectionType();
}
ngOnDestroy() {
if (this.selectedDevice) {
this.controller?.onDestroy(this.selectedDevice);
}
}
setEmitEvent(callback: EmitEvent) {
this.emitEvent = callback;
}
async onConnectionChange(event: MatSelectChange) {
this.changeHostConnection(event.value);
}
onDeviceClick(device: AdbDeviceConnection) {
this.selectedDevice = device;
this.onDevicesChange(assertDefined(this.controller).getDevices());
this.storage?.add(this.storeKeyLastDevice, device.id);
this.changeDetectorRef.detectChanges();
}
async onWinscopeEvent(event: WinscopeEvent) {
await event.visit(
WinscopeEventType.APP_REFRESH_DUMPS_REQUEST,
async (event) => {
this.targetTabIndex = 1;
this.dumpConfig = updateConfigsFromStore(
JSON.parse(JSON.stringify(assertDefined(this.dumpConfig))),
assertDefined(this.storage),
this.storeKeyPrefixDumpConfig,
);
this.refreshDumps = true;
},
);
}
onProgressUpdate(message: string, progressPercentage: number | undefined) {
if (
!LoadProgressComponent.canUpdateComponent(this.lastUiProgressUpdateTimeMs)
) {
return;
}
this.isExternalOperationInProgress = true;
this.progressMessage = message;
this.progressPercentage = progressPercentage;
this.lastUiProgressUpdateTimeMs = Date.now();
this.changeDetectorRef.detectChanges();
}
onOperationFinished(success: boolean) {
this.isExternalOperationInProgress = false;
this.lastUiProgressUpdateTimeMs = undefined;
if (!success) {
this.controller?.restartConnection();
}
this.changeDetectorRef.detectChanges();
}
isLoadOperationInProgress(): boolean {
return (
this.state === ConnectionState.LOADING_DATA ||
this.isExternalOperationInProgress
);
}
async onRetryConnection(token?: string) {
const controller = assertDefined(this.controller);
if (token !== undefined) {
controller.setSecurityToken(token);
}
await controller.restartConnection();
}
showAllDevices(): boolean {
const controller = assertDefined(this.controller);
if (this.state !== ConnectionState.IDLE) {
return false;
}
const devices = controller.getDevices();
const lastId = this.storage?.get(this.storeKeyLastDevice) ?? undefined;
if (this.selectedDevice) {
const newDevice = devices.find((d) => d.id === this.selectedDevice?.id);
if (newDevice && newDevice.getState() === AdbDeviceState.AVAILABLE) {
this.selectedDevice = newDevice;
} else {
this.selectedDevice = undefined;
}
}
if (this.selectedDevice === undefined && lastId !== undefined) {
const device = devices.find((d) => d.id === lastId);
if (device && device.getState() === AdbDeviceState.AVAILABLE) {
this.selectedDevice = device;
this.onDevicesChange(devices);
this.storage?.add(this.storeKeyLastDevice, device.id);
return false;
}
}
return this.selectedDevice === undefined;
}
showTraceCollectionConfig(): boolean {
if (this.selectedDevice === undefined) {
return false;
}
return this.state === ConnectionState.IDLE || this.isTracingOrLoading();
}
onTraceConfigChange(newConfig: TraceConfigurationMap) {
this.traceConfig = newConfig;
}
onDumpConfigChange(newConfig: TraceConfigurationMap) {
this.dumpConfig = newConfig;
}
async onChangeDeviceButton() {
this.storage?.add(this.storeKeyLastDevice, '');
this.selectedDevice = undefined;
await this.controller?.restartConnection();
}
async onRetryButton() {
await assertDefined(this.controller).restartConnection();
}
adbSuccess() {
return !this.notConnected.includes(this.state);
}
async startTracing() {
const requestedTraces = this.getRequests(assertDefined(this.traceConfig));
const imeReq = requestedTraces.includes(UiTraceTarget.IME);
const doNotShowDialog = !!this.storage?.get(this.storeKeyImeWarning);
if (!imeReq || doNotShowDialog) {
await this.requestTraces(requestedTraces);
return;
}
const sfReq = requestedTraces.includes(UiTraceTarget.SURFACE_FLINGER_TRACE);
const transactionsReq = requestedTraces.includes(
UiTraceTarget.TRANSACTIONS,
);
const wmReq = requestedTraces.includes(UiTraceTarget.WINDOW_MANAGER_TRACE);
const imeValidFrameMapping = sfReq && transactionsReq && wmReq;
if (imeValidFrameMapping) {
await this.requestTraces(requestedTraces);
return;
}
this.ngZone.run(() => {
const closeText = 'Collect traces anyway';
const optionText = 'Do not show again';
const data: WarningDialogData = {
message: `Cannot build frame mapping for IME with selected traces - some Winscope features may not work properly.
Consider the following selection for valid frame mapping:
Surface Flinger, Transactions, Window Manager, IME`,
actions: ['Go back'],
options: [optionText],
closeText,
};
const dialogRef = this.dialog.open(WarningDialogComponent, {
data,
disableClose: true,
});
dialogRef
.beforeClosed()
.subscribe((result: WarningDialogResult | undefined) => {
if (this.storage && result?.selectedOptions.includes(optionText)) {
this.storage.add(this.storeKeyImeWarning, 'true');
}
if (result?.closeActionText === closeText) {
this.requestTraces(requestedTraces);
}
});
});
}
async dumpState() {
const requestedDumps = this.getRequests(assertDefined(this.dumpConfig));
if (requestedDumps.length === 0) {
this.emitEvent(new NoTraceTargetsSelected());
return;
}
const requestedTraceTypes = requestedDumps.map((req) => {
return {
name: this.dumpConfig[req].name,
types: this.dumpConfig[req].types,
};
});
Analytics.Tracing.logCollectDumps(requestedTraceTypes.map((t) => t.name));
const requestedDumpsWithConfig: UserRequest[] = requestedDumps.map(
(target) => {
const enabledConfig = this.requestedEnabledConfig(
target,
this.dumpConfig,
);
const selectedConfig = this.requestedSelectedConfig(
target,
this.dumpConfig,
);
return {
target,
config: enabledConfig.concat(selectedConfig),
};
},
);
const controller = assertDefined(this.controller);
const device = assertDefined(this.selectedDevice);
await this.setState(ConnectionState.DUMPING_STATE);
await controller.dumpState(device, requestedDumpsWithConfig);
this.refreshDumps = false;
if (this.state === ConnectionState.DUMPING_STATE) {
this.filesCollected.emit({
requested: requestedTraceTypes,
collected: await this.fetchLastSessionData(),
});
}
}
async endTrace() {
if (!this.selectedDevice) {
return;
}
const controller = assertDefined(this.controller);
await this.setState(ConnectionState.ENDING_TRACE);
await controller.endTrace(this.selectedDevice);
if (this.state === ConnectionState.ENDING_TRACE) {
this.filesCollected.emit({
requested: this.requestedTraceTypes,
collected: await this.fetchLastSessionData(),
});
}
}
getDeviceName(device: AdbDeviceConnection): string {
return device.getFormattedName();
}
showTryAuthorizeButton(device: AdbDeviceConnection): boolean {
return (
device.getState() === AdbDeviceState.UNAUTHORIZED &&
this.getConnectionType() === AdbConnectionType.WDP
);
}
getSelectedDevice(): string {
return this.getDeviceName(assertDefined(this.selectedDevice));
}
getDeviceStateIcon(state: AdbDeviceState): string {
switch (state) {
case AdbDeviceState.AVAILABLE:
return 'smartphone';
case AdbDeviceState.UNAUTHORIZED:
return 'screen_lock_portrait';
case AdbDeviceState.OFFLINE:
return 'mobile_off';
default:
assertUnreachable(state);
}
}
isTracing(): boolean {
return this.tracingSessionStates.includes(this.state);
}
isTracingOrLoading(): boolean {
return this.isTracing() || this.isLoadOperationInProgress();
}
isDumpingState(): boolean {
return (
this.refreshDumps ||
this.state === ConnectionState.DUMPING_STATE ||
this.isLoadOperationInProgress()
);
}
disableTraceSection(): boolean {
return this.isTracingOrLoading() || this.refreshDumps;
}
async fetchExistingTraces() {
const controller = assertDefined(this.controller);
const files = await this.fetchLastSessionData();
this.filesCollected.emit({
requested: [],
collected: files,
});
if (files.length === 0) {
await controller.restartConnection();
}
}
onAvailableTracesChange(
newTraces: UiTraceTarget[],
removedTraces: UiTraceTarget[],
) {
newTraces.forEach((trace) => {
const config = assertDefined(this.traceConfig)[trace];
config.available = true;
});
removedTraces.forEach((trace) => {
const config = assertDefined(this.traceConfig)[trace];
config.available = false;
});
}
onDevicesChange(devices: AdbDeviceConnection[]) {
if (!this.selectedDevice) {
return;
}
const device = devices.find(
(d) => d.id === assertDefined(this.selectedDevice).id,
);
if (!device) {
return;
}
const screenRecordingConfig = assertDefined(this.traceConfig)[
UiTraceTarget.SCREEN_RECORDING
].config;
const displaysConfig = assertDefined(
screenRecordingConfig.selectionConfigs.find((c) => c.key === 'displays'),
);
const multiDisplay = device.hasMultiDisplayScreenRecording();
const displays = device.getDisplays();
if (multiDisplay && !Array.isArray(displaysConfig.value)) {
screenRecordingConfig.selectionConfigs =
makeScreenRecordingSelectionConfigs(displays, []);
} else if (!multiDisplay && Array.isArray(displaysConfig.value)) {
screenRecordingConfig.selectionConfigs =
makeScreenRecordingSelectionConfigs(displays, '');
} else {
screenRecordingConfig.selectionConfigs[0].options = displays;
}
const screenshotConfig = assertDefined(this.dumpConfig)[
UiTraceTarget.SCREENSHOT
].config;
assertDefined(
screenshotConfig.selectionConfigs.find((c) => c.key === 'displays'),
).options = displays;
this.changeDetectorRef.detectChanges();
}
async onError(errorText: string) {
await this.setState(ConnectionState.ERROR, errorText);
}
async onConnectionStateChange(newState: ConnectionState): Promise<void> {
switch (newState) {
case ConnectionState.IDLE:
if (this.state === ConnectionState.CONNECTING) {
await this.setState(newState);
}
return;
case ConnectionState.CONNECTING:
await this.setState(newState);
return;
default:
if (newState !== this.state) {
await this.setState(newState);
}
}
}
private async changeHostConnection(adbConnectionType: string) {
if (this.selectedDevice) {
await this.controller?.onDestroy(this.selectedDevice);
}
this.controller = new TraceCollectionController(adbConnectionType, this);
this.storage?.add(this.storeKeyAdbConnectionType, adbConnectionType);
await this.controller.restartConnection();
}
private async requestTraces(requestedTraces: UiTraceTarget[]) {
this.requestedTraceTypes = requestedTraces.map((req) => {
return {
name: this.traceConfig[req].name,
types: this.traceConfig[req].types,
};
});
Analytics.Tracing.logCollectTraces(
this.requestedTraceTypes.map((t) => t.name),
);
if (requestedTraces.length === 0) {
this.emitEvent(new NoTraceTargetsSelected());
return;
}
const requestedTracesWithConfig: UserRequest[] = requestedTraces.map(
(target) => {
const enabledConfig = this.requestedEnabledConfig(
target,
this.traceConfig,
);
const selectedConfig = this.requestedSelectedConfig(
target,
this.traceConfig,
);
return {
target,
config: enabledConfig.concat(selectedConfig),
};
},
);
const startTimeMs = Date.now();
await this.setState(ConnectionState.STARTING_TRACE);
await assertDefined(this.controller).startTrace(
assertDefined(this.selectedDevice),
requestedTracesWithConfig,
);
if (this.state === ConnectionState.STARTING_TRACE) {
Analytics.Tracing.logStartTime(Date.now() - startTimeMs);
await this.setState(ConnectionState.TRACING);
}
}
private async fetchLastSessionData() {
await this.setState(ConnectionState.LOADING_DATA);
const startTimeMs = Date.now();
const files = await assertDefined(this.controller).fetchLastSessionData(
assertDefined(this.selectedDevice),
);
if (files.length === 0) {
Analytics.Proxy.logNoFilesFound();
}
const size = files.reduce((total, file) => (total += file.size), 0);
Analytics.Loading.logFileExtractionTime(
'device',
Date.now() - startTimeMs,
size,
);
return files;
}
private getRequests(configMap: TraceConfigurationMap): UiTraceTarget[] {
return Object.keys(configMap)
.filter((dumpKey: string) => {
return configMap[dumpKey].config.enabled && dumpKey in UiTraceTarget;
})
.map((key) => Number(key)) as UiTraceTarget[];
}
private requestedEnabledConfig(
target: UiTraceTarget,
configMap: TraceConfigurationMap,
): UserRequestConfig[] {
const req: UserRequestConfig[] = [];
const trace = configMap[target];
assertTrue(trace?.config.enabled ?? false);
trace.config.checkboxConfigs.forEach((con: CheckboxConfiguration) => {
if (con.enabled) {
req.push({key: con.key});
}
});
return req;
}
private requestedSelectedConfig(
target: UiTraceTarget,
configMap: TraceConfigurationMap,
): UserRequestConfig[] {
const trace = configMap[target];
assertTrue(trace?.config.enabled ?? false);
return trace.config.selectionConfigs.map((con: SelectionConfiguration) => {
return {key: con.key, value: con.value};
});
}
private async setState(newState: ConnectionState, errorText = '') {
this.updateProgressMessage(newState);
const controller = assertDefined(this.controller);
this.state = newState;
this.errorText = errorText;
this.changeDetectorRef.detectChanges();
const maybeRefreshDumps =
this.refreshDumps &&
newState !== ConnectionState.LOADING_DATA &&
newState !== ConnectionState.CONNECTING;
if (
maybeRefreshDumps &&
newState === ConnectionState.IDLE &&
this.selectedDevice
) {
await this.dumpState();
} else if (maybeRefreshDumps) {
// device is not connected or proxy is not started/invalid/in error state
// so cannot refresh dump automatically
this.refreshDumps = false;
}
const deviceRequestStates = [
ConnectionState.IDLE,
ConnectionState.CONNECTING,
];
if (!deviceRequestStates.includes(newState)) {
controller.cancelDeviceRequests();
}
switch (newState) {
case ConnectionState.TRACE_TIMEOUT:
UserNotifier.add(new ProxyTraceTimeout());
await this.endTrace();
return;
case ConnectionState.NOT_FOUND:
Analytics.Proxy.logServerNotFound(controller.getConnectionType());
return;
case ConnectionState.ERROR:
Analytics.Error.logProxyError(this.errorText);
return;
case ConnectionState.CONNECTING:
await controller.requestDevices();
return;
case ConnectionState.IDLE: {
await this.selectedDevice?.updateAvailableTraces();
return;
}
default:
// do nothing
}
}
private updateProgressMessage(newState: ConnectionState) {
switch (newState) {
case ConnectionState.STARTING_TRACE:
this.progressMessage = 'Starting trace...';
this.progressIcon = 'cable';
this.progressPercentage = undefined;
break;
case ConnectionState.TRACING:
this.progressMessage = 'Tracing...';
this.progressIcon = 'cable';
this.progressPercentage = undefined;
break;
case ConnectionState.ENDING_TRACE:
this.progressMessage = 'Ending trace...';
this.progressIcon = 'cable';
break;
case ConnectionState.DUMPING_STATE:
this.progressMessage = 'Dumping state...';
this.progressIcon = 'cable';
break;
default:
this.progressIcon = 'sync';
}
}
}