• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {MatSelectChange} from '@angular/material/select';
29import {
30  assertDefined,
31  assertTrue,
32  assertUnreachable,
33} from 'common/assert_utils';
34import {FunctionUtils} from 'common/function_utils';
35import {Store} from 'common/store/store';
36import {UserNotifier} from 'common/user_notifier';
37import {Analytics} from 'logging/analytics';
38import {ProgressListener} from 'messaging/progress_listener';
39import {ProxyTraceTimeout} from 'messaging/user_warnings';
40import {
41  NoTraceTargetsSelected,
42  WinscopeEvent,
43  WinscopeEventType,
44} from 'messaging/winscope_event';
45import {
46  EmitEvent,
47  WinscopeEventEmitter,
48} from 'messaging/winscope_event_emitter';
49import {WinscopeEventListener} from 'messaging/winscope_event_listener';
50import {
51  AdbDeviceConnection,
52  AdbDeviceState,
53} from 'trace_collection/adb/adb_device_connection';
54import {AdbConnectionType} from 'trace_collection/adb_connection_type';
55import {AdbFiles, RequestedTraceTypes} from 'trace_collection/adb_files';
56import {ConnectionState} from 'trace_collection/connection_state';
57import {ConnectionStateListener} from 'trace_collection/connection_state_listener';
58import {TraceCollectionController} from 'trace_collection/controller/trace_collection_controller';
59import {
60  CheckboxConfiguration,
61  makeDefaultDumpConfigMap,
62  makeDefaultTraceConfigMap,
63  makeScreenRecordingSelectionConfigs,
64  SelectionConfiguration,
65  TraceConfigurationMap,
66  updateConfigsFromStore,
67} from 'trace_collection/ui/ui_trace_configuration';
68import {UiTraceTarget} from 'trace_collection/ui/ui_trace_target';
69import {UserRequest, UserRequestConfig} from 'trace_collection/user_request';
70import {LoadProgressComponent} from './load_progress_component';
71import {
72  WarningDialogComponent,
73  WarningDialogData,
74  WarningDialogResult,
75} from './warning_dialog_component';
76
77@Component({
78  selector: 'collect-traces',
79  template: `
80    <mat-card class="collect-card">
81      <mat-card-title class="title">Collect Traces</mat-card-title>
82
83      <mat-card-content *ngIf="controller" class="collect-card-content">
84        <mat-form-field class="connection-type">
85          <mat-label>Select connection type</mat-label>
86          <mat-select
87            [value]="getConnectionType()"
88            (selectionChange)="onConnectionChange($event)"
89            [disabled]="disableTraceSection()">
90            <mat-option [value]="AdbConnectionType.WINSCOPE_PROXY">
91                <span>{{AdbConnectionType.WINSCOPE_PROXY}}</span>
92              </mat-option>
93            <mat-option [value]="AdbConnectionType.WDP">
94                <span>{{AdbConnectionType.WDP}}</span>
95              </mat-option>
96          </mat-select>
97        </mat-form-field>
98
99        <button
100          mat-icon-button
101          class="refresh-connection"
102          (click)="onRetryConnection()"
103          matTooltip="Refresh connection"><mat-icon>refresh</mat-icon></button>
104
105        <ng-container *ngIf="!adbSuccess()">
106          <winscope-proxy-setup
107            *ngIf="getConnectionType() === AdbConnectionType.WINSCOPE_PROXY"
108            [state]="state"
109            (retryConnection)="onRetryConnection($event)"></winscope-proxy-setup>
110          <wdp-setup
111            *ngIf="getConnectionType() === AdbConnectionType.WDP"
112            [state]="state"
113            (retryConnection)="onRetryConnection()"></wdp-setup>
114        </ng-container>
115
116        <div *ngIf="showAllDevices()" class="devices-connecting">
117          <div
118            *ngIf="controller.getDevices().length === 0"
119            class="no-device-detected">
120            <p class="mat-body-3 icon">
121              <mat-icon inline fontIcon="phonelink_erase"></mat-icon>
122            </p>
123            <p class="mat-body-1">No devices detected</p>
124          </div>
125          <div
126            *ngIf="controller.getDevices().length > 0"
127            class="device-selection">
128            <p class="mat-body-1 instruction">Select a device:</p>
129            <mat-list>
130              <mat-list-item
131                *ngFor="let device of controller.getDevices()"
132                [disabled]="device.state === ${AdbDeviceState.OFFLINE}"
133                (click)="onDeviceClick(device)"
134                class="available-device">
135                <mat-icon matListIcon>
136                  {{ getDeviceStateIcon(device.state) }}
137                </mat-icon>
138                <p matLine>
139                  {{ getDeviceName(device) }}
140                </p>
141                <mat-icon
142                  *ngIf="showTryAuthorizeButton(device)"
143                  class="material-symbols-outlined authorize-btn"
144                  matTooltip="Authorize device"
145                  (click)="device.tryAuthorize()">lock_open</mat-icon>
146              </mat-list-item>
147            </mat-list>
148          </div>
149        </div>
150
151        <div
152          *ngIf="showTraceCollectionConfig()"
153          class="trace-collection-config">
154          <mat-list>
155            <mat-list-item class="selected-device">
156              <mat-icon matListIcon>smartphone</mat-icon>
157              <p matLine>
158                {{ getSelectedDevice()}}
159              </p>
160
161              <div class="device-actions">
162                <button
163                  color="primary"
164                  class="change-btn"
165                  mat-stroked-button
166                  (click)="onChangeDeviceButton()"
167                  [disabled]="isTracingOrLoading()">
168                  Change device
169                </button>
170                <button
171                  color="primary"
172                  class="fetch-btn"
173                  mat-stroked-button
174                  (click)="fetchExistingTraces()"
175                  [disabled]="isTracingOrLoading()">
176                  Fetch traces from last session
177                </button>
178              </div>
179            </mat-list-item>
180          </mat-list>
181
182          <mat-tab-group [selectedIndex]="targetTabIndex" class="target-tabs">
183            <mat-tab
184              label="Trace"
185              [disabled]="disableTraceSection()">
186              <div class="tabbed-section">
187                <div
188                  class="trace-section"
189                  *ngIf="state === ${ConnectionState.IDLE}">
190                  <trace-config
191                    title="Trace targets"
192                    [traceConfig]="traceConfig"
193                    [storage]="storage"
194                    [traceConfigStoreKey]="storeKeyPrefixTraceConfig"
195                    (traceConfigChange)="onTraceConfigChange($event)"></trace-config>
196                  <div class="start-btn">
197                    <button
198                      color="primary"
199                      mat-raised-button
200                      (click)="startTracing()">Start trace</button>
201                  </div>
202                </div>
203
204                <div *ngIf="isTracingOrLoading()" class="tracing-progress">
205                  <load-progress
206                    [icon]="progressIcon"
207                    [message]="progressMessage"
208                    [progressPercentage]="progressPercentage">
209                  </load-progress>
210                  <div class="end-btn" *ngIf="isTracing()">
211                    <button
212                      color="primary"
213                      mat-raised-button
214                      [disabled]="state !== ${ConnectionState.TRACING}"
215                      (click)="endTrace()">
216                      End trace
217                    </button>
218                  </div>
219                </div>
220              </div>
221            </mat-tab>
222            <mat-tab
223              label="Dump"
224              [disabled]="isTracingOrLoading()">
225              <div class="tabbed-section">
226                <div
227                  class="dump-section"
228                  *ngIf="state === ${ConnectionState.IDLE} && !refreshDumps">
229                  <trace-config
230                    title="Dump targets"
231                    [traceConfig]="dumpConfig"
232                    [storage]="storage"
233                    [traceConfigStoreKey]="storeKeyPrefixDumpConfig"
234                    (traceConfigChange)="onDumpConfigChange($event)"></trace-config>
235                  <div class="dump-btn" *ngIf="!refreshDumps">
236                    <button
237                      color="primary"
238                      mat-raised-button
239                      (click)="dumpState()">Dump state</button>
240                  </div>
241                </div>
242
243                <load-progress
244                  class="dumping-state"
245                  *ngIf="isDumpingState()"
246                  [progressPercentage]="progressPercentage"
247                  [message]="progressMessage">
248                </load-progress>
249              </div>
250            </mat-tab>
251          </mat-tab-group>
252        </div>
253
254        <div *ngIf="state === ${ConnectionState.ERROR}" class="unknown-error">
255          <p class="error-wrapper mat-body-1">
256            <mat-icon class="error-icon">error</mat-icon>
257            Error:
258          </p>
259          <pre> {{ errorText }} </pre>
260          <button
261            color="primary"
262            class="retry-btn"
263            mat-raised-button
264            (click)="onRetryButton()">Retry</button>
265        </div>
266      </mat-card-content>
267    </mat-card>
268  `,
269  styles: [
270    `
271      .change-btn,
272      .retry-btn,
273      .fetch-btn {
274        margin-left: 5px;
275      }
276      .fetch-btn {
277        margin-top: 5px;
278      }
279      .selected-device {
280        height: fit-content !important;
281      }
282      .mat-card.collect-card {
283        display: flex;
284      }
285      .collect-card {
286        height: 100%;
287        flex-direction: column;
288        overflow: auto;
289        margin: 10px;
290      }
291      .collect-card-content {
292        overflow: auto;
293      }
294      .selection {
295        display: flex;
296        flex-direction: row;
297        flex-wrap: wrap;
298        gap: 10px;
299      }
300      .trace-collection-config,
301      .trace-section,
302      .dump-section,
303      .tracing-progress,
304      trace-config {
305        display: flex;
306        flex-direction: column;
307        gap: 10px;
308      }
309      .trace-section,
310      .dump-section,
311      .tracing-progress {
312        height: 100%;
313      }
314      .winscope-proxy-setup-tab,
315      .web-tab,
316      .start-btn,
317      .dump-btn,
318      .end-btn {
319        align-self: flex-start;
320      }
321      .start-btn,
322      .dump-btn,
323      .end-btn {
324        margin: auto 0 0 0;
325        padding: 1rem 0 0 0;
326      }
327      .error-wrapper {
328        display: flex;
329        flex-direction: row;
330        align-items: center;
331      }
332      .error-icon {
333        margin-right: 5px;
334      }
335      .available-device {
336        cursor: pointer;
337      }
338
339      .no-device-detected {
340        display: flex;
341        flex-direction: column;
342        justify-content: center;
343        align-content: center;
344        align-items: center;
345        height: 100%;
346      }
347
348      .no-device-detected p,
349      .device-selection p.instruction {
350        padding-top: 1rem;
351        opacity: 0.6;
352        font-size: 1.2rem;
353      }
354
355      .no-device-detected .icon {
356        font-size: 3rem;
357        margin: 0 0 0.2rem 0;
358      }
359
360      mat-card-content {
361        flex-grow: 1;
362      }
363
364      mat-tab-body {
365        padding: 1rem;
366      }
367
368      .loading-info {
369        opacity: 0.8;
370        padding: 1rem 0;
371      }
372
373      .target-tabs {
374        flex-grow: 1;
375      }
376
377      .target-tabs .mat-tab-body-wrapper {
378        flex-grow: 1;
379      }
380
381      .tabbed-section {
382        height: 100%;
383      }
384
385      .progress-desc {
386        display: flex;
387        height: 100%;
388        flex-direction: column;
389        justify-content: center;
390        align-content: center;
391        align-items: center;
392      }
393
394      .progress-desc > * {
395        max-width: 250px;
396      }
397
398      load-progress {
399        height: 100%;
400      }
401    `,
402  ],
403  encapsulation: ViewEncapsulation.None,
404})
405export class CollectTracesComponent
406  implements
407    ProgressListener,
408    WinscopeEventListener,
409    WinscopeEventEmitter,
410    ConnectionStateListener
411{
412  objectKeys = Object.keys;
413  AdbConnectionType = AdbConnectionType;
414  isExternalOperationInProgress = false;
415  progressMessage = 'Fetching...';
416  progressIcon = 'sync';
417  progressPercentage: number | undefined;
418  lastUiProgressUpdateTimeMs?: number;
419  refreshDumps = false;
420  targetTabIndex = 0;
421  traceConfig: TraceConfigurationMap;
422  dumpConfig: TraceConfigurationMap;
423  requestedTraceTypes: RequestedTraceTypes[] = [];
424  controller: TraceCollectionController | undefined;
425  state = ConnectionState.CONNECTING;
426  errorText = '';
427
428  readonly storeKeyPrefixTraceConfig = 'TraceSettings.';
429  readonly storeKeyPrefixDumpConfig = 'DumpSettings.';
430  private readonly storeKeyImeWarning = 'doNotShowImeWarningDialog';
431  private readonly storeKeyLastDevice = 'adb.lastDevice';
432  private readonly storeKeyAdbConnectionType = 'adbConnectionType';
433
434  private selectedDevice: AdbDeviceConnection | undefined;
435  private emitEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
436
437  private readonly notConnected = [
438    ConnectionState.CONNECTING,
439    ConnectionState.NOT_FOUND,
440    ConnectionState.UNAUTH,
441    ConnectionState.INVALID_VERSION,
442  ];
443  private readonly tracingSessionStates = [
444    ConnectionState.STARTING_TRACE,
445    ConnectionState.TRACING,
446    ConnectionState.ENDING_TRACE,
447    ConnectionState.DUMPING_STATE,
448  ];
449
450  @Input() storage: Store | undefined;
451  @Output() readonly filesCollected = new EventEmitter<AdbFiles>();
452
453  constructor(
454    @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef,
455    @Inject(MatDialog) private dialog: MatDialog,
456    @Inject(NgZone) private ngZone: NgZone,
457  ) {
458    this.traceConfig = makeDefaultTraceConfigMap();
459    this.dumpConfig = makeDefaultDumpConfigMap();
460  }
461
462  async ngOnInit() {
463    const adbConnectionType = this.storage?.get(this.storeKeyAdbConnectionType);
464    if (adbConnectionType !== undefined) {
465      await this.changeHostConnection(adbConnectionType);
466    } else {
467      await this.changeHostConnection(AdbConnectionType.WINSCOPE_PROXY);
468    }
469  }
470
471  getConnectionType() {
472    return this.controller?.getConnectionType();
473  }
474
475  ngOnDestroy() {
476    if (this.selectedDevice) {
477      this.controller?.onDestroy(this.selectedDevice);
478    }
479  }
480
481  setEmitEvent(callback: EmitEvent) {
482    this.emitEvent = callback;
483  }
484
485  async onConnectionChange(event: MatSelectChange) {
486    this.changeHostConnection(event.value);
487  }
488
489  onDeviceClick(device: AdbDeviceConnection) {
490    this.selectedDevice = device;
491    this.onDevicesChange(assertDefined(this.controller).getDevices());
492    this.storage?.add(this.storeKeyLastDevice, device.id);
493    this.changeDetectorRef.detectChanges();
494  }
495
496  async onWinscopeEvent(event: WinscopeEvent) {
497    await event.visit(
498      WinscopeEventType.APP_REFRESH_DUMPS_REQUEST,
499      async (event) => {
500        this.targetTabIndex = 1;
501        this.dumpConfig = updateConfigsFromStore(
502          JSON.parse(JSON.stringify(assertDefined(this.dumpConfig))),
503          assertDefined(this.storage),
504          this.storeKeyPrefixDumpConfig,
505        );
506        this.refreshDumps = true;
507      },
508    );
509  }
510
511  onProgressUpdate(message: string, progressPercentage: number | undefined) {
512    if (
513      !LoadProgressComponent.canUpdateComponent(this.lastUiProgressUpdateTimeMs)
514    ) {
515      return;
516    }
517    this.isExternalOperationInProgress = true;
518    this.progressMessage = message;
519    this.progressPercentage = progressPercentage;
520    this.lastUiProgressUpdateTimeMs = Date.now();
521    this.changeDetectorRef.detectChanges();
522  }
523
524  onOperationFinished(success: boolean) {
525    this.isExternalOperationInProgress = false;
526    this.lastUiProgressUpdateTimeMs = undefined;
527    if (!success) {
528      this.controller?.restartConnection();
529    }
530    this.changeDetectorRef.detectChanges();
531  }
532
533  isLoadOperationInProgress(): boolean {
534    return (
535      this.state === ConnectionState.LOADING_DATA ||
536      this.isExternalOperationInProgress
537    );
538  }
539
540  async onRetryConnection(token?: string) {
541    const controller = assertDefined(this.controller);
542    if (token !== undefined) {
543      controller.setSecurityToken(token);
544    }
545    await controller.restartConnection();
546  }
547
548  showAllDevices(): boolean {
549    const controller = assertDefined(this.controller);
550    if (this.state !== ConnectionState.IDLE) {
551      return false;
552    }
553
554    const devices = controller.getDevices();
555    const lastId = this.storage?.get(this.storeKeyLastDevice) ?? undefined;
556
557    if (this.selectedDevice) {
558      const newDevice = devices.find((d) => d.id === this.selectedDevice?.id);
559      if (newDevice && newDevice.getState() === AdbDeviceState.AVAILABLE) {
560        this.selectedDevice = newDevice;
561      } else {
562        this.selectedDevice = undefined;
563      }
564    }
565
566    if (this.selectedDevice === undefined && lastId !== undefined) {
567      const device = devices.find((d) => d.id === lastId);
568      if (device && device.getState() === AdbDeviceState.AVAILABLE) {
569        this.selectedDevice = device;
570        this.onDevicesChange(devices);
571        this.storage?.add(this.storeKeyLastDevice, device.id);
572        return false;
573      }
574    }
575
576    return this.selectedDevice === undefined;
577  }
578
579  showTraceCollectionConfig(): boolean {
580    if (this.selectedDevice === undefined) {
581      return false;
582    }
583    return this.state === ConnectionState.IDLE || this.isTracingOrLoading();
584  }
585
586  onTraceConfigChange(newConfig: TraceConfigurationMap) {
587    this.traceConfig = newConfig;
588  }
589
590  onDumpConfigChange(newConfig: TraceConfigurationMap) {
591    this.dumpConfig = newConfig;
592  }
593
594  async onChangeDeviceButton() {
595    this.storage?.add(this.storeKeyLastDevice, '');
596    this.selectedDevice = undefined;
597    await this.controller?.restartConnection();
598  }
599
600  async onRetryButton() {
601    await assertDefined(this.controller).restartConnection();
602  }
603
604  adbSuccess() {
605    return !this.notConnected.includes(this.state);
606  }
607
608  async startTracing() {
609    const requestedTraces = this.getRequests(assertDefined(this.traceConfig));
610    const imeReq = requestedTraces.includes(UiTraceTarget.IME);
611    const doNotShowDialog = !!this.storage?.get(this.storeKeyImeWarning);
612
613    if (!imeReq || doNotShowDialog) {
614      await this.requestTraces(requestedTraces);
615      return;
616    }
617
618    const sfReq = requestedTraces.includes(UiTraceTarget.SURFACE_FLINGER_TRACE);
619    const transactionsReq = requestedTraces.includes(
620      UiTraceTarget.TRANSACTIONS,
621    );
622    const wmReq = requestedTraces.includes(UiTraceTarget.WINDOW_MANAGER_TRACE);
623    const imeValidFrameMapping = sfReq && transactionsReq && wmReq;
624
625    if (imeValidFrameMapping) {
626      await this.requestTraces(requestedTraces);
627      return;
628    }
629
630    this.ngZone.run(() => {
631      const closeText = 'Collect traces anyway';
632      const optionText = 'Do not show again';
633      const data: WarningDialogData = {
634        message: `Cannot build frame mapping for IME with selected traces - some Winscope features may not work properly.
635        Consider the following selection for valid frame mapping:
636        Surface Flinger, Transactions, Window Manager, IME`,
637        actions: ['Go back'],
638        options: [optionText],
639        closeText,
640      };
641      const dialogRef = this.dialog.open(WarningDialogComponent, {
642        data,
643        disableClose: true,
644      });
645      dialogRef
646        .beforeClosed()
647        .subscribe((result: WarningDialogResult | undefined) => {
648          if (this.storage && result?.selectedOptions.includes(optionText)) {
649            this.storage.add(this.storeKeyImeWarning, 'true');
650          }
651          if (result?.closeActionText === closeText) {
652            this.requestTraces(requestedTraces);
653          }
654        });
655    });
656  }
657
658  async dumpState() {
659    const requestedDumps = this.getRequests(assertDefined(this.dumpConfig));
660    if (requestedDumps.length === 0) {
661      this.emitEvent(new NoTraceTargetsSelected());
662      return;
663    }
664
665    const requestedTraceTypes = requestedDumps.map((req) => {
666      return {
667        name: this.dumpConfig[req].name,
668        types: this.dumpConfig[req].types,
669      };
670    });
671    Analytics.Tracing.logCollectDumps(requestedTraceTypes.map((t) => t.name));
672
673    const requestedDumpsWithConfig: UserRequest[] = requestedDumps.map(
674      (target) => {
675        const enabledConfig = this.requestedEnabledConfig(
676          target,
677          this.dumpConfig,
678        );
679        const selectedConfig = this.requestedSelectedConfig(
680          target,
681          this.dumpConfig,
682        );
683        return {
684          target,
685          config: enabledConfig.concat(selectedConfig),
686        };
687      },
688    );
689
690    const controller = assertDefined(this.controller);
691    const device = assertDefined(this.selectedDevice);
692    await this.setState(ConnectionState.DUMPING_STATE);
693    await controller.dumpState(device, requestedDumpsWithConfig);
694    this.refreshDumps = false;
695    if (this.state === ConnectionState.DUMPING_STATE) {
696      this.filesCollected.emit({
697        requested: requestedTraceTypes,
698        collected: await this.fetchLastSessionData(),
699      });
700    }
701  }
702
703  async endTrace() {
704    if (!this.selectedDevice) {
705      return;
706    }
707    const controller = assertDefined(this.controller);
708    await this.setState(ConnectionState.ENDING_TRACE);
709    await controller.endTrace(this.selectedDevice);
710    if (this.state === ConnectionState.ENDING_TRACE) {
711      this.filesCollected.emit({
712        requested: this.requestedTraceTypes,
713        collected: await this.fetchLastSessionData(),
714      });
715    }
716  }
717
718  getDeviceName(device: AdbDeviceConnection): string {
719    return device.getFormattedName();
720  }
721
722  showTryAuthorizeButton(device: AdbDeviceConnection): boolean {
723    return (
724      device.getState() === AdbDeviceState.UNAUTHORIZED &&
725      this.getConnectionType() === AdbConnectionType.WDP
726    );
727  }
728
729  getSelectedDevice(): string {
730    return this.getDeviceName(assertDefined(this.selectedDevice));
731  }
732
733  getDeviceStateIcon(state: AdbDeviceState): string {
734    switch (state) {
735      case AdbDeviceState.AVAILABLE:
736        return 'smartphone';
737      case AdbDeviceState.UNAUTHORIZED:
738        return 'screen_lock_portrait';
739      case AdbDeviceState.OFFLINE:
740        return 'mobile_off';
741      default:
742        assertUnreachable(state);
743    }
744  }
745
746  isTracing(): boolean {
747    return this.tracingSessionStates.includes(this.state);
748  }
749
750  isTracingOrLoading(): boolean {
751    return this.isTracing() || this.isLoadOperationInProgress();
752  }
753
754  isDumpingState(): boolean {
755    return (
756      this.refreshDumps ||
757      this.state === ConnectionState.DUMPING_STATE ||
758      this.isLoadOperationInProgress()
759    );
760  }
761
762  disableTraceSection(): boolean {
763    return this.isTracingOrLoading() || this.refreshDumps;
764  }
765
766  async fetchExistingTraces() {
767    const controller = assertDefined(this.controller);
768    const files = await this.fetchLastSessionData();
769    this.filesCollected.emit({
770      requested: [],
771      collected: files,
772    });
773    if (files.length === 0) {
774      await controller.restartConnection();
775    }
776  }
777
778  onAvailableTracesChange(
779    newTraces: UiTraceTarget[],
780    removedTraces: UiTraceTarget[],
781  ) {
782    newTraces.forEach((trace) => {
783      const config = assertDefined(this.traceConfig)[trace];
784      config.available = true;
785    });
786    removedTraces.forEach((trace) => {
787      const config = assertDefined(this.traceConfig)[trace];
788      config.available = false;
789    });
790  }
791
792  onDevicesChange(devices: AdbDeviceConnection[]) {
793    if (!this.selectedDevice) {
794      return;
795    }
796    const device = devices.find(
797      (d) => d.id === assertDefined(this.selectedDevice).id,
798    );
799    if (!device) {
800      return;
801    }
802    const screenRecordingConfig = assertDefined(this.traceConfig)[
803      UiTraceTarget.SCREEN_RECORDING
804    ].config;
805    const displaysConfig = assertDefined(
806      screenRecordingConfig.selectionConfigs.find((c) => c.key === 'displays'),
807    );
808    const multiDisplay = device.hasMultiDisplayScreenRecording();
809    const displays = device.getDisplays();
810
811    if (multiDisplay && !Array.isArray(displaysConfig.value)) {
812      screenRecordingConfig.selectionConfigs =
813        makeScreenRecordingSelectionConfigs(displays, []);
814    } else if (!multiDisplay && Array.isArray(displaysConfig.value)) {
815      screenRecordingConfig.selectionConfigs =
816        makeScreenRecordingSelectionConfigs(displays, '');
817    } else {
818      screenRecordingConfig.selectionConfigs[0].options = displays;
819    }
820
821    const screenshotConfig = assertDefined(this.dumpConfig)[
822      UiTraceTarget.SCREENSHOT
823    ].config;
824    assertDefined(
825      screenshotConfig.selectionConfigs.find((c) => c.key === 'displays'),
826    ).options = displays;
827    this.changeDetectorRef.detectChanges();
828  }
829
830  async onError(errorText: string) {
831    await this.setState(ConnectionState.ERROR, errorText);
832  }
833
834  async onConnectionStateChange(newState: ConnectionState): Promise<void> {
835    switch (newState) {
836      case ConnectionState.IDLE:
837        if (this.state === ConnectionState.CONNECTING) {
838          await this.setState(newState);
839        }
840        return;
841      case ConnectionState.CONNECTING:
842        await this.setState(newState);
843        return;
844      default:
845        if (newState !== this.state) {
846          await this.setState(newState);
847        }
848    }
849  }
850
851  private async changeHostConnection(adbConnectionType: string) {
852    if (this.selectedDevice) {
853      await this.controller?.onDestroy(this.selectedDevice);
854    }
855    this.controller = new TraceCollectionController(adbConnectionType, this);
856    this.storage?.add(this.storeKeyAdbConnectionType, adbConnectionType);
857    await this.controller.restartConnection();
858  }
859
860  private async requestTraces(requestedTraces: UiTraceTarget[]) {
861    this.requestedTraceTypes = requestedTraces.map((req) => {
862      return {
863        name: this.traceConfig[req].name,
864        types: this.traceConfig[req].types,
865      };
866    });
867    Analytics.Tracing.logCollectTraces(
868      this.requestedTraceTypes.map((t) => t.name),
869    );
870
871    if (requestedTraces.length === 0) {
872      this.emitEvent(new NoTraceTargetsSelected());
873      return;
874    }
875
876    const requestedTracesWithConfig: UserRequest[] = requestedTraces.map(
877      (target) => {
878        const enabledConfig = this.requestedEnabledConfig(
879          target,
880          this.traceConfig,
881        );
882        const selectedConfig = this.requestedSelectedConfig(
883          target,
884          this.traceConfig,
885        );
886        return {
887          target,
888          config: enabledConfig.concat(selectedConfig),
889        };
890      },
891    );
892    const startTimeMs = Date.now();
893    await this.setState(ConnectionState.STARTING_TRACE);
894    await assertDefined(this.controller).startTrace(
895      assertDefined(this.selectedDevice),
896      requestedTracesWithConfig,
897    );
898    if (this.state === ConnectionState.STARTING_TRACE) {
899      Analytics.Tracing.logStartTime(Date.now() - startTimeMs);
900      await this.setState(ConnectionState.TRACING);
901    }
902  }
903
904  private async fetchLastSessionData() {
905    await this.setState(ConnectionState.LOADING_DATA);
906    const startTimeMs = Date.now();
907    const files = await assertDefined(this.controller).fetchLastSessionData(
908      assertDefined(this.selectedDevice),
909    );
910    if (files.length === 0) {
911      Analytics.Proxy.logNoFilesFound();
912    }
913    const size = files.reduce((total, file) => (total += file.size), 0);
914    Analytics.Loading.logFileExtractionTime(
915      'device',
916      Date.now() - startTimeMs,
917      size,
918    );
919    return files;
920  }
921
922  private getRequests(configMap: TraceConfigurationMap): UiTraceTarget[] {
923    return Object.keys(configMap)
924      .filter((dumpKey: string) => {
925        return configMap[dumpKey].config.enabled && dumpKey in UiTraceTarget;
926      })
927      .map((key) => Number(key)) as UiTraceTarget[];
928  }
929
930  private requestedEnabledConfig(
931    target: UiTraceTarget,
932    configMap: TraceConfigurationMap,
933  ): UserRequestConfig[] {
934    const req: UserRequestConfig[] = [];
935    const trace = configMap[target];
936    assertTrue(trace?.config.enabled ?? false);
937    trace.config.checkboxConfigs.forEach((con: CheckboxConfiguration) => {
938      if (con.enabled) {
939        req.push({key: con.key});
940      }
941    });
942    return req;
943  }
944
945  private requestedSelectedConfig(
946    target: UiTraceTarget,
947    configMap: TraceConfigurationMap,
948  ): UserRequestConfig[] {
949    const trace = configMap[target];
950    assertTrue(trace?.config.enabled ?? false);
951    return trace.config.selectionConfigs.map((con: SelectionConfiguration) => {
952      return {key: con.key, value: con.value};
953    });
954  }
955
956  private async setState(newState: ConnectionState, errorText = '') {
957    this.updateProgressMessage(newState);
958
959    const controller = assertDefined(this.controller);
960
961    this.state = newState;
962    this.errorText = errorText;
963    this.changeDetectorRef.detectChanges();
964
965    const maybeRefreshDumps =
966      this.refreshDumps &&
967      newState !== ConnectionState.LOADING_DATA &&
968      newState !== ConnectionState.CONNECTING;
969    if (
970      maybeRefreshDumps &&
971      newState === ConnectionState.IDLE &&
972      this.selectedDevice
973    ) {
974      await this.dumpState();
975    } else if (maybeRefreshDumps) {
976      // device is not connected or proxy is not started/invalid/in error state
977      // so cannot refresh dump automatically
978      this.refreshDumps = false;
979    }
980
981    const deviceRequestStates = [
982      ConnectionState.IDLE,
983      ConnectionState.CONNECTING,
984    ];
985    if (!deviceRequestStates.includes(newState)) {
986      controller.cancelDeviceRequests();
987    }
988
989    switch (newState) {
990      case ConnectionState.TRACE_TIMEOUT:
991        UserNotifier.add(new ProxyTraceTimeout());
992        await this.endTrace();
993        return;
994      case ConnectionState.NOT_FOUND:
995        Analytics.Proxy.logServerNotFound(controller.getConnectionType());
996        return;
997
998      case ConnectionState.ERROR:
999        Analytics.Error.logProxyError(this.errorText);
1000        return;
1001
1002      case ConnectionState.CONNECTING:
1003        await controller.requestDevices();
1004        return;
1005
1006      case ConnectionState.IDLE: {
1007        await this.selectedDevice?.updateAvailableTraces();
1008        return;
1009      }
1010      default:
1011      // do nothing
1012    }
1013  }
1014
1015  private updateProgressMessage(newState: ConnectionState) {
1016    switch (newState) {
1017      case ConnectionState.STARTING_TRACE:
1018        this.progressMessage = 'Starting trace...';
1019        this.progressIcon = 'cable';
1020        this.progressPercentage = undefined;
1021        break;
1022      case ConnectionState.TRACING:
1023        this.progressMessage = 'Tracing...';
1024        this.progressIcon = 'cable';
1025        this.progressPercentage = undefined;
1026        break;
1027      case ConnectionState.ENDING_TRACE:
1028        this.progressMessage = 'Ending trace...';
1029        this.progressIcon = 'cable';
1030        break;
1031      case ConnectionState.DUMPING_STATE:
1032        this.progressMessage = 'Dumping state...';
1033        this.progressIcon = 'cable';
1034        break;
1035      default:
1036        this.progressIcon = 'sync';
1037    }
1038  }
1039}
1040