/* * 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, OnDestroy, OnInit, Output, ViewEncapsulation, } from '@angular/core'; import {assertDefined} from 'common/assert_utils'; import {PersistentStoreProxy} from 'common/persistent_store_proxy'; import {Analytics} from 'logging/analytics'; import {ProgressListener} from 'messaging/progress_listener'; import {WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event'; import {WinscopeEventListener} from 'messaging/winscope_event_listener'; import {Connection} from 'trace_collection/connection'; import {ProxyState} from 'trace_collection/proxy_client'; import {ProxyConnection} from 'trace_collection/proxy_connection'; import { ConfigMap, EnableConfiguration, SelectionConfiguration, TraceConfigurationMap, } from 'trace_collection/trace_collection_utils'; import {LoadProgressComponent} from './load_progress_component'; @Component({ selector: 'collect-traces', template: ` Collect Traces

Connecting...

No devices detected

Select a device:

{{ connect.devices()[deviceId].authorised ? 'smartphone' : 'screen_lock_portrait' }}

{{ connect.devices()[deviceId].authorised ? connect.devices()[deviceId]?.model : 'unauthorised' }} ({{ deviceId }})

smartphone

{{ connect.selectedDevice()?.model }} ({{ connect.selectedDeviceId() }})

Dump targets

{{ dumpConfig[dumpKey].name }}

error Error:

 {{ connect.proxy?.errorText }} 
`, styles: [ ` .change-btn, .retry-btn { margin-left: 5px; } .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; } .set-up-adb, .trace-collection-config, .trace-section, .dump-section, .starting-trace, .end-tracing, .load-data, trace-config { display: flex; flex-direction: column; gap: 10px; } .trace-section, .dump-section, .starting-trace, .end-tracing, .load-data { height: 100%; } .trace-collection-config { height: 100%; } .proxy-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; } .devices-connecting { height: 100%; } mat-card-content { flex-grow: 1; } mat-tab-body { padding: 1rem; } .loading-info { opacity: 0.8; padding: 1rem 0; } .tracing-tabs { flex-grow: 1; } .tracing-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 OnInit, OnDestroy, ProgressListener, WinscopeEventListener { objectKeys = Object.keys; isAdbProxy = true; connect: Connection | undefined; isExternalOperationInProgress = false; progressMessage = 'Fetching...'; progressPercentage: number | undefined; lastUiProgressUpdateTimeMs?: number; refreshDumps = false; selectedTabIndex = 0; @Input() traceConfig: TraceConfigurationMap | undefined; @Input() dumpConfig: TraceConfigurationMap | undefined; @Input() storage: Storage | undefined; @Output() readonly filesCollected = new EventEmitter(); constructor( @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef, ) {} ngOnInit() { if (this.isAdbProxy) { this.connect = new ProxyConnection( (newState) => this.onProxyStateChange(), (progress) => this.onLoadProgressUpdate(progress), this.setTraceConfigForAvailableTraces, ); } else { // TODO: change to WebAdbConnection this.connect = new ProxyConnection( (newState) => this.onProxyStateChange(), (progress) => this.onLoadProgressUpdate(progress), this.setTraceConfigForAvailableTraces, ); } } ngOnDestroy(): void { assertDefined(this.connect).proxy?.removeOnProxyChange(this.onProxyChange); } async onDeviceClick(deviceId: string) { await assertDefined(this.connect).selectDevice(deviceId); } async onWinscopeEvent(event: WinscopeEvent) { await event.visit( WinscopeEventType.APP_REFRESH_DUMPS_REQUEST, async (event) => { this.selectedTabIndex = 1; this.progressMessage = 'Refreshing dumps...'; this.progressPercentage = 0; 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() { this.isExternalOperationInProgress = false; this.lastUiProgressUpdateTimeMs = undefined; this.changeDetectorRef.detectChanges(); } isOperationInProgress(): boolean { return ( assertDefined(this.connect).isLoadDataState() || this.isExternalOperationInProgress ); } async onAddKey(key: string) { if (this.connect?.setProxyKey) { this.connect.setProxyKey(key); } await assertDefined(this.connect).restart(); } displayAdbProxyTab() { this.isAdbProxy = true; this.connect = new ProxyConnection( (newState) => this.onProxyStateChange(), (progress) => this.onLoadProgressUpdate(progress), this.setTraceConfigForAvailableTraces, ); } displayWebAdbTab() { this.isAdbProxy = false; //TODO: change to WebAdbConnection this.connect = new ProxyConnection( (newState) => this.onProxyStateChange(), (progress) => this.onLoadProgressUpdate(progress), this.setTraceConfigForAvailableTraces, ); } showTraceCollectionConfig() { const connect = assertDefined(this.connect); return ( connect.isStartTraceState() || connect.isStartingTraceState() || connect.isEndTraceState() || this.isOperationInProgress() ); } async onChangeDeviceButton() { await assertDefined(this.connect).resetLastDevice(); } async onRetryButton() { await assertDefined(this.connect).restart(); } async startTracing() { console.log('begin tracing'); const requestedTraces = this.getRequestedTraces(); Analytics.Tracing.logCollectTraces(requestedTraces); const reqEnableConfig = this.requestedEnableConfig(); const reqSelectedSfConfig = this.requestedSelection('layers_trace'); const reqSelectedWmConfig = this.requestedSelection('window_trace'); if (requestedTraces.length < 1) { await assertDefined(this.connect).throwNoTargetsError(); return; } await assertDefined(this.connect).startTrace( requestedTraces, reqEnableConfig, reqSelectedSfConfig, reqSelectedWmConfig, ); } async dumpState() { console.log('begin dump'); const requestedDumps = this.getRequestedDumps(); Analytics.Tracing.logCollectDumps(requestedDumps); const dumpSuccessful = await assertDefined(this.connect).dumpState( requestedDumps, ); this.refreshDumps = false; if (dumpSuccessful) { this.filesCollected.emit(assertDefined(this.connect).adbData()); } } async endTrace() { console.log('end tracing'); await assertDefined(this.connect).endTrace(); this.filesCollected.emit(assertDefined(this.connect).adbData()); } tabClass(adbTab: boolean) { let isActive: string; if (adbTab) { isActive = this.isAdbProxy ? 'active' : 'inactive'; } else { isActive = !this.isAdbProxy ? 'active' : 'inactive'; } return ['tab', isActive]; } private async onProxyChange(newState: ProxyState) { await assertDefined(this.connect).onConnectChange.bind(this.connect)( newState, ); } private onProxyStateChange() { this.changeDetectorRef.detectChanges(); if ( !this.refreshDumps || this.connect?.isLoadDataState() || this.connect?.isConnectingState() ) { return; } if (this.connect?.isStartTraceState()) { this.dumpState(); } else { // device is not connected or proxy is not started/invalid/in error state // so cannot refresh dump automatically this.refreshDumps = false; } } private getRequestedTraces() { const tracesFromCollection: string[] = []; const tracingConfig = assertDefined(this.traceConfig); const requested = Object.keys(tracingConfig).filter((traceKey: string) => { return tracingConfig[traceKey].run; }); requested.push(...tracesFromCollection); requested.push('perfetto_trace'); // always start/stop/fetch perfetto trace return requested; } private getRequestedDumps() { const dumpConfig = assertDefined(this.dumpConfig); const requested = Object.keys(dumpConfig).filter((dumpKey: string) => { return dumpConfig[dumpKey].run; }); requested.push('perfetto_dump'); // always dump/fetch perfetto dump return requested; } private requestedEnableConfig(): string[] { const req: string[] = []; const tracingConfig = assertDefined(this.traceConfig); Object.keys(tracingConfig).forEach((traceKey: string) => { const trace = tracingConfig[traceKey]; if (trace.run && trace.config && trace.config.enableConfigs) { trace.config.enableConfigs.forEach((con: EnableConfiguration) => { if (con.enabled) { req.push(con.key); } }); } }); return req; } private requestedSelection(traceType: string): ConfigMap | undefined { const tracingConfig = assertDefined(this.traceConfig); if (!tracingConfig[traceType].run) { return undefined; } const selected: ConfigMap = {}; tracingConfig[traceType].config?.selectionConfigs.forEach( (con: SelectionConfiguration) => { selected[con.key] = con.value; }, ); return selected; } private onLoadProgressUpdate(progressPercentage: number) { this.progressPercentage = progressPercentage; this.changeDetectorRef.detectChanges(); } private setTraceConfigForAvailableTraces = ( availableTracesConfig: TraceConfigurationMap, ) => (this.traceConfig = PersistentStoreProxy.new( 'TraceConfiguration', availableTracesConfig, assertDefined(this.storage), )); }