xref: /aosp_15_r20/development/tools/winscope/src/app/components/collect_traces_component.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1/*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {
18  ChangeDetectorRef,
19  Component,
20  EventEmitter,
21  Inject,
22  Input,
23  NgZone,
24  Output,
25  ViewEncapsulation,
26} from '@angular/core';
27import {MatDialog} from '@angular/material/dialog';
28import {assertDefined} from 'common/assert_utils';
29import {FunctionUtils} from 'common/function_utils';
30import {PersistentStoreProxy} from 'common/persistent_store_proxy';
31import {Store} from 'common/store';
32import {UserNotifier} from 'common/user_notifier';
33import {Analytics} from 'logging/analytics';
34import {ProgressListener} from 'messaging/progress_listener';
35import {ProxyTraceTimeout} from 'messaging/user_warnings';
36import {
37  NoTraceTargetsSelected,
38  WinscopeEvent,
39  WinscopeEventType,
40} from 'messaging/winscope_event';
41import {
42  EmitEvent,
43  WinscopeEventEmitter,
44} from 'messaging/winscope_event_emitter';
45import {WinscopeEventListener} from 'messaging/winscope_event_listener';
46import {AdbConnection} from 'trace_collection/adb_connection';
47import {AdbDevice} from 'trace_collection/adb_device';
48import {AdbFiles, RequestedTraceTypes} from 'trace_collection/adb_files';
49import {ConnectionState} from 'trace_collection/connection_state';
50import {ProxyConnection} from 'trace_collection/proxy_connection';
51import {
52  EnableConfiguration,
53  makeDefaultDumpConfigMap,
54  makeDefaultTraceConfigMap,
55  makeScreenRecordingConfigs,
56  SelectionConfiguration,
57  TraceConfigurationMap,
58} from 'trace_collection/trace_configuration';
59import {TraceRequest, TraceRequestConfig} from 'trace_collection/trace_request';
60import {LoadProgressComponent} from './load_progress_component';
61import {
62  WarningDialogComponent,
63  WarningDialogData,
64  WarningDialogResult,
65} from './warning_dialog_component';
66
67@Component({
68  selector: 'collect-traces',
69  template: `
70    <mat-card class="collect-card">
71      <mat-card-title class="title">Collect Traces</mat-card-title>
72
73      <mat-card-content *ngIf="adbConnection" class="collect-card-content">
74        <p *ngIf="adbConnection.getState() === ${ConnectionState.CONNECTING}" class="connecting-message mat-body-1">
75          Connecting...
76        </p>
77
78        <div *ngIf="!adbSuccess()" class="set-up-adb">
79          <button
80            class="proxy-tab"
81            color="primary"
82            mat-stroked-button
83            [ngClass]="tabClass(true)">
84            ADB Proxy
85          </button>
86          <!-- <button class="web-tab" color="primary" mat-raised-button [ngClass]="tabClass(false)" (click)="displayWebAdbTab()">Web ADB</button> -->
87          <adb-proxy
88            *ngIf="isAdbProxy()"
89            [state]="adbConnection.getState()"
90            (retryConnection)="onRetryConnection($event)"></adb-proxy>
91          <!-- <web-adb *ngIf="!isAdbProxy()"></web-adb> TODO: fix web adb workflow -->
92        </div>
93
94        <div *ngIf="showAllDevices()" class="devices-connecting">
95          <div *ngIf="adbConnection.getDevices().length === 0" class="no-device-detected">
96            <p class="mat-body-3 icon"><mat-icon inline fontIcon="phonelink_erase"></mat-icon></p>
97            <p class="mat-body-1">No devices detected</p>
98          </div>
99          <div *ngIf="adbConnection.getDevices().length > 0" class="device-selection">
100            <p class="mat-body-1 instruction">Select a device:</p>
101            <mat-list>
102              <mat-list-item
103                *ngFor="let device of adbConnection.getDevices()"
104                (click)="onDeviceClick(device)"
105                class="available-device">
106                <mat-icon matListIcon>
107                  {{
108                    device.authorized ? 'smartphone' : 'screen_lock_portrait'
109                  }}
110                </mat-icon>
111                <p matLine>
112                  {{
113                    device.authorized
114                      ? device.model
115                      : 'unauthorized'
116                  }}
117                  ({{ device.id }})
118                </p>
119              </mat-list-item>
120            </mat-list>
121          </div>
122        </div>
123
124        <div
125          *ngIf="showTraceCollectionConfig()"
126          class="trace-collection-config">
127          <mat-list>
128            <mat-list-item class="selected-device">
129              <mat-icon matListIcon>smartphone</mat-icon>
130              <p matLine>
131                {{ getSelectedDevice()}}
132              </p>
133
134              <div class="device-actions">
135                <button
136                  color="primary"
137                  class="change-btn"
138                  mat-stroked-button
139                  (click)="onChangeDeviceButton()"
140                  [disabled]="isTracingOrLoading()">
141                  Change device
142                </button>
143                <button
144                  color="primary"
145                  class="fetch-btn"
146                  mat-stroked-button
147                  (click)="fetchExistingTraces()"
148                  [disabled]="isTracingOrLoading()">
149                  Fetch traces from last session
150                </button>
151              </div>
152            </mat-list-item>
153          </mat-list>
154
155          <mat-tab-group [selectedIndex]="selectedTabIndex" class="tracing-tabs">
156            <mat-tab
157              label="Trace"
158              [disabled]="disableTraceSection()">
159              <div class="tabbed-section">
160                <div class="trace-section" *ngIf="adbConnection.getState() === ${ConnectionState.IDLE}">
161                  <trace-config
162                    title="Trace targets"
163                    [initialTraceConfig]="traceConfig"
164                    [storage]="storage"
165                    traceConfigStoreKey="TraceSettings"
166                    (traceConfigChange)="onTraceConfigChange($event)"></trace-config>
167                  <div class="start-btn">
168                    <button color="primary" mat-raised-button (click)="startTracing()">
169                      Start trace
170                    </button>
171                  </div>
172                </div>
173
174                <div *ngIf="adbConnection.getState() === ${ConnectionState.STARTING_TRACE}" class="starting-trace">
175                  <load-progress
176                    message="Starting trace...">
177                  </load-progress>
178                  <div class="end-btn">
179                    <button color="primary" mat-raised-button [disabled]="true">
180                      End trace
181                    </button>
182                  </div>
183                </div>
184
185                <div *ngIf="adbConnection.getState() === ${ConnectionState.TRACING}" class="tracing">
186                  <load-progress
187                    icon="cable"
188                    message="Tracing...">
189                  </load-progress>
190                  <div class="end-btn">
191                    <button color="primary" mat-raised-button (click)="endTrace()">
192                      End trace
193                    </button>
194                  </div>
195                </div>
196
197                <div *ngIf="adbConnection.getState() === ${ConnectionState.ENDING_TRACE}" class="ending-trace">
198                  <load-progress
199                    icon="cable"
200                    message="Ending trace...">
201                  </load-progress>
202                  <div class="end-btn">
203                    <button color="primary" mat-raised-button [disabled]="true">
204                      End trace
205                    </button>
206                  </div>
207                </div>
208
209                <div *ngIf="isLoadOperationInProgress()" class="load-data">
210                  <load-progress
211                    [progressPercentage]="progressPercentage"
212                    [message]="progressMessage">
213                  </load-progress>
214                  <div class="end-btn">
215                    <button color="primary" mat-raised-button [disabled]="true">
216                      End trace
217                    </button>
218                  </div>
219                </div>
220              </div>
221            </mat-tab>
222            <mat-tab label="Dump" [disabled]="isTracingOrLoading()">
223              <div class="tabbed-section">
224                <div class="dump-section" *ngIf="adbConnection.getState() === ${ConnectionState.IDLE} && !refreshDumps">
225                  <trace-config
226                    title="Dump targets"
227                    [initialTraceConfig]="dumpConfig"
228                    [storage]="storage"
229                    [traceConfigStoreKey]="storeKeyDumpConfig"
230                    (traceConfigChange)="onDumpConfigChange($event)"></trace-config>
231                  <div class="dump-btn" *ngIf="!refreshDumps">
232                    <button color="primary" mat-raised-button (click)="dumpState()">
233                      Dump state
234                    </button>
235                  </div>
236                </div>
237
238                <load-progress
239                  class="dumping-state"
240                  *ngIf="isDumpingState()"
241                  [progressPercentage]="progressPercentage"
242                  [message]="progressMessage">
243                </load-progress>
244              </div>
245            </mat-tab>
246          </mat-tab-group>
247        </div>
248
249        <div *ngIf="adbConnection.getState() === ${ConnectionState.ERROR}" class="unknown-error">
250          <p class="error-wrapper mat-body-1">
251            <mat-icon class="error-icon">error</mat-icon>
252            Error:
253          </p>
254          <pre> {{ adbConnection.getErrorText() }} </pre>
255          <button color="primary" class="retry-btn" mat-raised-button (click)="onRetryButton()">
256            Retry
257          </button>
258        </div>
259      </mat-card-content>
260    </mat-card>
261  `,
262  styles: [
263    `
264      .change-btn,
265      .retry-btn,
266      .fetch-btn {
267        margin-left: 5px;
268      }
269      .fetch-btn {
270        margin-top: 5px;
271      }
272      .selected-device {
273        height: fit-content !important;
274      }
275      .mat-card.collect-card {
276        display: flex;
277      }
278      .collect-card {
279        height: 100%;
280        flex-direction: column;
281        overflow: auto;
282        margin: 10px;
283      }
284      .collect-card-content {
285        overflow: auto;
286      }
287      .selection {
288        display: flex;
289        flex-direction: row;
290        flex-wrap: wrap;
291        gap: 10px;
292      }
293      .set-up-adb,
294      .trace-collection-config,
295      .trace-section,
296      .dump-section,
297      .starting-trace,
298      .tracing,
299      .ending-trace,
300      .load-data,
301      trace-config {
302        display: flex;
303        flex-direction: column;
304        gap: 10px;
305      }
306      .trace-section,
307      .dump-section,
308      .starting-trace,
309      .tracing,
310      .ending-trace,
311      .load-data {
312        height: 100%;
313      }
314      .trace-collection-config {
315        height: 100%;
316      }
317      .proxy-tab,
318      .web-tab,
319      .start-btn,
320      .dump-btn,
321      .end-btn {
322        align-self: flex-start;
323      }
324      .start-btn,
325      .dump-btn,
326      .end-btn {
327        margin: auto 0 0 0;
328        padding: 1rem 0 0 0;
329      }
330      .error-wrapper {
331        display: flex;
332        flex-direction: row;
333        align-items: center;
334      }
335      .error-icon {
336        margin-right: 5px;
337      }
338      .available-device {
339        cursor: pointer;
340      }
341
342      .no-device-detected {
343        display: flex;
344        flex-direction: column;
345        justify-content: center;
346        align-content: center;
347        align-items: center;
348        height: 100%;
349      }
350
351      .no-device-detected p,
352      .device-selection p.instruction {
353        padding-top: 1rem;
354        opacity: 0.6;
355        font-size: 1.2rem;
356      }
357
358      .no-device-detected .icon {
359        font-size: 3rem;
360        margin: 0 0 0.2rem 0;
361      }
362
363      .devices-connecting {
364        height: 100%;
365      }
366
367      mat-card-content {
368        flex-grow: 1;
369      }
370
371      mat-tab-body {
372        padding: 1rem;
373      }
374
375      .loading-info {
376        opacity: 0.8;
377        padding: 1rem 0;
378      }
379
380      .tracing-tabs {
381        flex-grow: 1;
382      }
383
384      .tracing-tabs .mat-tab-body-wrapper {
385        flex-grow: 1;
386      }
387
388      .tabbed-section {
389        height: 100%;
390      }
391
392      .progress-desc {
393        display: flex;
394        height: 100%;
395        flex-direction: column;
396        justify-content: center;
397        align-content: center;
398        align-items: center;
399      }
400
401      .progress-desc > * {
402        max-width: 250px;
403      }
404
405      load-progress {
406        height: 100%;
407      }
408    `,
409  ],
410  encapsulation: ViewEncapsulation.None,
411})
412export class CollectTracesComponent
413  implements ProgressListener, WinscopeEventListener, WinscopeEventEmitter
414{
415  objectKeys = Object.keys;
416  isExternalOperationInProgress = false;
417  progressMessage = 'Fetching...';
418  progressPercentage: number | undefined;
419  lastUiProgressUpdateTimeMs?: number;
420  refreshDumps = false;
421  selectedTabIndex = 0;
422  traceConfig: TraceConfigurationMap;
423  dumpConfig: TraceConfigurationMap;
424  requestedTraceTypes: RequestedTraceTypes[] = [];
425
426  private readonly storeKeyImeWarning = 'doNotShowImeWarningDialog';
427  private readonly storeKeyLastDevice = 'adb.lastDevice';
428  private readonly storeKeyDumpConfig = 'DumpSettings';
429
430  private selectedDevice: AdbDevice | undefined;
431  private emitEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
432
433  private readonly notConnected = [
434    ConnectionState.NOT_FOUND,
435    ConnectionState.UNAUTH,
436    ConnectionState.INVALID_VERSION,
437  ];
438
439  @Input() adbConnection: AdbConnection | undefined;
440  @Input() storage: Store | undefined;
441
442  @Output() readonly filesCollected = new EventEmitter<AdbFiles>();
443
444  constructor(
445    @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef,
446    @Inject(MatDialog) private dialog: MatDialog,
447    @Inject(NgZone) private ngZone: NgZone,
448  ) {
449    this.traceConfig = makeDefaultTraceConfigMap();
450    this.dumpConfig = makeDefaultDumpConfigMap();
451  }
452
453  ngOnChanges() {
454    if (!this.adbConnection) {
455      throw new Error('component created without adb connection');
456    }
457    this.adbConnection.initialize(
458      () => this.onConnectionStateChange(),
459      this.toggleAvailabilityOfTraces,
460      this.handleDevicesChange,
461    );
462  }
463
464  ngOnDestroy() {
465    this.adbConnection?.onDestroy();
466  }
467
468  setEmitEvent(callback: EmitEvent) {
469    this.emitEvent = callback;
470  }
471
472  onDeviceClick(device: AdbDevice) {
473    this.selectedDevice = device;
474    this.storage?.add(this.storeKeyLastDevice, device.id);
475    this.changeDetectorRef.detectChanges();
476  }
477
478  async onWinscopeEvent(event: WinscopeEvent) {
479    await event.visit(
480      WinscopeEventType.APP_REFRESH_DUMPS_REQUEST,
481      async (event) => {
482        this.selectedTabIndex = 1;
483        this.dumpConfig = PersistentStoreProxy.new<TraceConfigurationMap>(
484          assertDefined('DumpSettings'),
485          assertDefined(
486            JSON.parse(JSON.stringify(assertDefined(this.dumpConfig))),
487          ),
488          assertDefined(this.storage),
489        );
490        this.refreshDumps = true;
491      },
492    );
493  }
494
495  onProgressUpdate(message: string, progressPercentage: number | undefined) {
496    if (
497      !LoadProgressComponent.canUpdateComponent(this.lastUiProgressUpdateTimeMs)
498    ) {
499      return;
500    }
501    this.isExternalOperationInProgress = true;
502    this.progressMessage = message;
503    this.progressPercentage = progressPercentage;
504    this.lastUiProgressUpdateTimeMs = Date.now();
505    this.changeDetectorRef.detectChanges();
506  }
507
508  onOperationFinished(success: boolean) {
509    this.isExternalOperationInProgress = false;
510    this.lastUiProgressUpdateTimeMs = undefined;
511    if (!success) {
512      this.adbConnection?.restartConnection();
513    }
514    this.changeDetectorRef.detectChanges();
515  }
516
517  isLoadOperationInProgress(): boolean {
518    return (
519      assertDefined(this.adbConnection).getState() ===
520        ConnectionState.LOADING_DATA || this.isExternalOperationInProgress
521    );
522  }
523
524  async onRetryConnection(token: string) {
525    const connection = assertDefined(this.adbConnection);
526    connection.setSecurityToken(token);
527    await connection.restartConnection();
528  }
529
530  showAllDevices(): boolean {
531    const connection = assertDefined(this.adbConnection);
532    const state = connection.getState();
533    if (state !== ConnectionState.IDLE) {
534      return false;
535    }
536
537    const devices = connection.getDevices();
538    const lastId = this.storage?.get(this.storeKeyLastDevice) ?? undefined;
539    if (
540      this.selectedDevice &&
541      !devices.find((d) => d.id === this.selectedDevice?.id)
542    ) {
543      this.selectedDevice = undefined;
544    }
545
546    if (this.selectedDevice === undefined && lastId !== undefined) {
547      const device = devices.find((d) => d.id === lastId);
548      if (device && device.authorized) {
549        this.selectedDevice = device;
550        this.storage?.add(this.storeKeyLastDevice, device.id);
551        return false;
552      }
553    }
554
555    return this.selectedDevice === undefined;
556  }
557
558  showTraceCollectionConfig(): boolean {
559    if (this.selectedDevice === undefined) {
560      return false;
561    }
562    return (
563      assertDefined(this.adbConnection).getState() === ConnectionState.IDLE ||
564      this.isTracingOrLoading()
565    );
566  }
567
568  onTraceConfigChange(newConfig: TraceConfigurationMap) {
569    this.traceConfig = newConfig;
570  }
571
572  onDumpConfigChange(newConfig: TraceConfigurationMap) {
573    this.dumpConfig = newConfig;
574  }
575
576  async onChangeDeviceButton() {
577    this.storage?.add(this.storeKeyLastDevice, '');
578    this.selectedDevice = undefined;
579    await this.adbConnection?.restartConnection();
580  }
581
582  async onRetryButton() {
583    await assertDefined(this.adbConnection).restartConnection();
584  }
585
586  adbSuccess() {
587    const state = this.adbConnection?.getState();
588    return !!state && !this.notConnected.includes(state);
589  }
590
591  async startTracing() {
592    const requestedTraces = this.getRequestedTraces();
593
594    const imeReq = requestedTraces.includes('ime');
595    const doNotShowDialog = !!this.storage?.get(this.storeKeyImeWarning);
596
597    if (!imeReq || doNotShowDialog) {
598      await this.requestTraces(requestedTraces);
599      return;
600    }
601
602    const sfReq = requestedTraces.includes('layers_trace');
603    const transactionsReq = requestedTraces.includes('transactions');
604    const wmReq = requestedTraces.includes('window_trace');
605    const imeValidFrameMapping = sfReq && transactionsReq && wmReq;
606
607    if (imeValidFrameMapping) {
608      await this.requestTraces(requestedTraces);
609      return;
610    }
611
612    this.ngZone.run(() => {
613      const closeText = 'Collect traces anyway';
614      const optionText = 'Do not show again';
615      const data: WarningDialogData = {
616        message: `Cannot build frame mapping for IME with selected traces - some Winscope features may not work properly.
617        Consider the following selection for valid frame mapping:
618        Surface Flinger, Transactions, Window Manager, IME`,
619        actions: ['Go back'],
620        options: [optionText],
621        closeText,
622      };
623      const dialogRef = this.dialog.open(WarningDialogComponent, {
624        data,
625        disableClose: true,
626      });
627      dialogRef
628        .beforeClosed()
629        .subscribe((result: WarningDialogResult | undefined) => {
630          if (this.storage && result?.selectedOptions.includes(optionText)) {
631            this.storage.add(this.storeKeyImeWarning, 'true');
632          }
633          if (result?.closeActionText === closeText) {
634            this.requestTraces(requestedTraces);
635          }
636        });
637    });
638  }
639
640  async dumpState() {
641    const requestedDumps = this.getRequestedDumps();
642    const requestedTraceTypes = requestedDumps.map((req) => {
643      return {
644        name: this.dumpConfig[req].name,
645        types: this.dumpConfig[req].types,
646      };
647    });
648    Analytics.Tracing.logCollectDumps(requestedDumps);
649
650    if (requestedDumps.length === 0) {
651      this.emitEvent(new NoTraceTargetsSelected());
652      return;
653    }
654    requestedDumps.push('perfetto_dump'); // always dump/fetch perfetto dump
655
656    const requestedDumpsWithConfig: TraceRequest[] = requestedDumps.map(
657      (dumpName) => {
658        const enabledConfig = this.requestedEnabledConfig(
659          dumpName,
660          this.dumpConfig,
661        );
662        const selectedConfig = this.requestedSelectedConfig(
663          dumpName,
664          this.dumpConfig,
665        );
666        return {
667          name: dumpName,
668          config: enabledConfig.concat(selectedConfig),
669        };
670      },
671    );
672
673    this.progressMessage = 'Dumping state...';
674
675    const connection = assertDefined(this.adbConnection);
676    const device = assertDefined(this.selectedDevice);
677    await connection.dumpState(device, requestedDumpsWithConfig);
678    this.refreshDumps = false;
679    if (connection.getState() === ConnectionState.DUMPING_STATE) {
680      this.filesCollected.emit({
681        requested: requestedTraceTypes,
682        collected: await connection.fetchLastTracingSessionData(device),
683      });
684    }
685  }
686
687  async endTrace() {
688    const connection = assertDefined(this.adbConnection);
689    const device = assertDefined(this.selectedDevice);
690    await connection.endTrace(device);
691    if (connection.getState() === ConnectionState.ENDING_TRACE) {
692      this.filesCollected.emit({
693        requested: this.requestedTraceTypes,
694        collected: await connection.fetchLastTracingSessionData(device),
695      });
696    }
697  }
698
699  isAdbProxy(): boolean {
700    return this.adbConnection instanceof ProxyConnection;
701  }
702
703  tabClass(adbTab: boolean) {
704    let isActive: string;
705    if (adbTab) {
706      isActive = this.isAdbProxy() ? 'active' : 'inactive';
707    } else {
708      isActive = !this.isAdbProxy() ? 'active' : 'inactive';
709    }
710    return ['tab', isActive];
711  }
712
713  getSelectedDevice(): string {
714    const device = assertDefined(this.selectedDevice);
715    return device.model + `(${device.id})`;
716  }
717
718  isTracingOrLoading(): boolean {
719    const state = this.adbConnection?.getState();
720    const tracingStates = [
721      ConnectionState.STARTING_TRACE,
722      ConnectionState.TRACING,
723      ConnectionState.ENDING_TRACE,
724      ConnectionState.DUMPING_STATE,
725    ];
726    return (
727      (!!state && tracingStates.includes(state)) ||
728      this.isLoadOperationInProgress()
729    );
730  }
731
732  isDumpingState(): boolean {
733    return (
734      this.refreshDumps ||
735      this.adbConnection?.getState() === ConnectionState.DUMPING_STATE ||
736      this.isLoadOperationInProgress()
737    );
738  }
739
740  disableTraceSection(): boolean {
741    return this.isTracingOrLoading() || this.refreshDumps;
742  }
743
744  async fetchExistingTraces() {
745    const connection = assertDefined(this.adbConnection);
746    const files = await connection.fetchLastTracingSessionData(
747      assertDefined(this.selectedDevice),
748    );
749    this.filesCollected.emit({
750      requested: [],
751      collected: files,
752    });
753    if (files.length === 0) {
754      await connection.restartConnection();
755    }
756  }
757
758  private async requestTraces(requestedTraces: string[]) {
759    this.requestedTraceTypes = requestedTraces.map((req) => {
760      return {
761        name: this.traceConfig[req].name,
762        types: this.traceConfig[req].types,
763      };
764    });
765    Analytics.Tracing.logCollectTraces(requestedTraces);
766
767    if (requestedTraces.length === 0) {
768      this.emitEvent(new NoTraceTargetsSelected());
769      return;
770    }
771    requestedTraces.push('perfetto_trace'); // always start/stop/fetch perfetto trace
772
773    const requestedTracesWithConfig: TraceRequest[] = requestedTraces.map(
774      (traceName) => {
775        const enabledConfig = this.requestedEnabledConfig(
776          traceName,
777          this.traceConfig,
778        );
779        const selectedConfig = this.requestedSelectedConfig(
780          traceName,
781          this.traceConfig,
782        );
783        return {
784          name: traceName,
785          config: enabledConfig.concat(selectedConfig),
786        };
787      },
788    );
789    await assertDefined(this.adbConnection).startTrace(
790      assertDefined(this.selectedDevice),
791      requestedTracesWithConfig,
792    );
793  }
794
795  private async onConnectionStateChange() {
796    this.changeDetectorRef.detectChanges();
797
798    const connection = assertDefined(this.adbConnection);
799    const state = connection.getState();
800    if (state === ConnectionState.TRACE_TIMEOUT) {
801      UserNotifier.add(new ProxyTraceTimeout());
802      this.filesCollected.emit({
803        requested: this.requestedTraceTypes,
804        collected: await connection.fetchLastTracingSessionData(
805          assertDefined(this.selectedDevice),
806        ),
807      });
808      return;
809    }
810
811    if (
812      !this.refreshDumps ||
813      state === ConnectionState.LOADING_DATA ||
814      state === ConnectionState.CONNECTING
815    ) {
816      return;
817    }
818    if (state === ConnectionState.IDLE && this.selectedDevice) {
819      this.dumpState();
820    } else {
821      // device is not connected or proxy is not started/invalid/in error state
822      // so cannot refresh dump automatically
823      this.refreshDumps = false;
824    }
825  }
826
827  private getRequestedTraces(): string[] {
828    const tracingConfig = assertDefined(this.traceConfig);
829    return Object.keys(tracingConfig).filter((traceKey: string) => {
830      return tracingConfig[traceKey].enabled;
831    });
832  }
833
834  private getRequestedDumps(): string[] {
835    let dumpConfig = assertDefined(this.dumpConfig);
836    if (this.refreshDumps && this.storage) {
837      const storedConfig = this.storage.get(this.storeKeyDumpConfig);
838      if (storedConfig) {
839        dumpConfig = JSON.parse(storedConfig);
840      }
841    }
842    return Object.keys(dumpConfig).filter((dumpKey: string) => {
843      return dumpConfig[dumpKey].enabled;
844    });
845  }
846
847  private requestedEnabledConfig(
848    traceName: string,
849    configMap: TraceConfigurationMap,
850  ): TraceRequestConfig[] {
851    const req: TraceRequestConfig[] = [];
852    const trace = configMap[traceName];
853    if (trace?.enabled) {
854      trace.config?.enableConfigs?.forEach((con: EnableConfiguration) => {
855        if (con.enabled) {
856          req.push({key: con.key});
857        }
858      });
859    }
860    return req;
861  }
862
863  private requestedSelectedConfig(
864    traceName: string,
865    configMap: TraceConfigurationMap,
866  ): TraceRequestConfig[] {
867    const trace = configMap[traceName];
868    if (!trace?.enabled) {
869      return [];
870    }
871    return (
872      trace.config?.selectionConfigs.map((con: SelectionConfiguration) => {
873        return {key: con.key, value: con.value};
874      }) ?? []
875    );
876  }
877
878  private toggleAvailabilityOfTraces = (traces: string[]) =>
879    traces.forEach((trace) => {
880      const config = assertDefined(this.traceConfig)[trace];
881      config.available = !config.available;
882    });
883
884  private handleDevicesChange = (devices: AdbDevice[]) => {
885    if (!this.selectedDevice) {
886      return;
887    }
888    const selectedDevice = devices.find(
889      (d) => d.id === assertDefined(this.selectedDevice).id,
890    );
891    if (!selectedDevice) {
892      return;
893    }
894    const screenRecordingConfig = assertDefined(
895      this.traceConfig['screen_recording'].config,
896    );
897    const displays = assertDefined(
898      screenRecordingConfig?.selectionConfigs.find((c) => c.key === 'displays'),
899    );
900    if (
901      selectedDevice.multiDisplayScreenRecordingAvailable &&
902      !Array.isArray(displays.value)
903    ) {
904      screenRecordingConfig.selectionConfigs = makeScreenRecordingConfigs(
905        selectedDevice.displays,
906        [],
907      );
908    } else if (
909      !selectedDevice.multiDisplayScreenRecordingAvailable &&
910      Array.isArray(displays.value)
911    ) {
912      screenRecordingConfig.selectionConfigs = makeScreenRecordingConfigs(
913        selectedDevice.displays,
914        '',
915      );
916    } else {
917      screenRecordingConfig.selectionConfigs[0].options =
918        selectedDevice.displays;
919    }
920
921    const screenshotConfig = assertDefined(this.dumpConfig)['screenshot']
922      .config;
923    assertDefined(
924      screenshotConfig?.selectionConfigs.find((c) => c.key === 'displays'),
925    ).options = selectedDevice.displays;
926
927    this.changeDetectorRef.detectChanges();
928  };
929}
930