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  OnDestroy,
24  OnInit,
25  Output,
26  ViewEncapsulation,
27} from '@angular/core';
28import {assertDefined} from 'common/assert_utils';
29import {PersistentStoreProxy} from 'common/persistent_store_proxy';
30import {Analytics} from 'logging/analytics';
31import {ProgressListener} from 'messaging/progress_listener';
32import {WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event';
33import {WinscopeEventListener} from 'messaging/winscope_event_listener';
34import {Connection} from 'trace_collection/connection';
35import {ProxyState} from 'trace_collection/proxy_client';
36import {ProxyConnection} from 'trace_collection/proxy_connection';
37import {
38  ConfigMap,
39  EnableConfiguration,
40  SelectionConfiguration,
41  TraceConfigurationMap,
42} from 'trace_collection/trace_collection_utils';
43import {LoadProgressComponent} from './load_progress_component';
44
45@Component({
46  selector: 'collect-traces',
47  template: `
48    <mat-card class="collect-card">
49      <mat-card-title class="title">Collect Traces</mat-card-title>
50
51      <mat-card-content class="collect-card-content">
52        <p *ngIf="connect.isConnectingState()" class="connecting-message mat-body-1">
53          Connecting...
54        </p>
55
56        <div *ngIf="!connect.adbSuccess()" class="set-up-adb">
57          <button
58            class="proxy-tab"
59            color="primary"
60            mat-stroked-button
61            [ngClass]="tabClass(true)"
62            (click)="displayAdbProxyTab()">
63            ADB Proxy
64          </button>
65          <!-- <button class="web-tab" color="primary" mat-raised-button [ngClass]="tabClass(false)" (click)="displayWebAdbTab()">Web ADB</button> -->
66          <adb-proxy
67            *ngIf="isAdbProxy"
68            [(proxy)]="connect.proxy"
69            (addKey)="onAddKey($event)"></adb-proxy>
70          <!-- <web-adb *ngIf="!isAdbProxy"></web-adb> TODO: fix web adb workflow -->
71        </div>
72
73        <div *ngIf="connect.isDevicesState()" class="devices-connecting">
74          <div *ngIf="objectKeys(connect.devices()).length === 0" class="no-device-detected">
75            <p class="mat-body-3 icon"><mat-icon inline fontIcon="phonelink_erase"></mat-icon></p>
76            <p class="mat-body-1">No devices detected</p>
77          </div>
78          <div *ngIf="objectKeys(connect.devices()).length > 0" class="device-selection">
79            <p class="mat-body-1 instruction">Select a device:</p>
80            <mat-list *ngIf="objectKeys(connect.devices()).length > 0">
81              <mat-list-item
82                *ngFor="let deviceId of objectKeys(connect.devices())"
83                (click)="onDeviceClick(deviceId)"
84                class="available-device">
85                <mat-icon matListIcon>
86                  {{
87                    connect.devices()[deviceId].authorised ? 'smartphone' : 'screen_lock_portrait'
88                  }}
89                </mat-icon>
90                <p matLine>
91                  {{
92                    connect.devices()[deviceId].authorised
93                      ? connect.devices()[deviceId]?.model
94                      : 'unauthorised'
95                  }}
96                  ({{ deviceId }})
97                </p>
98              </mat-list-item>
99            </mat-list>
100          </div>
101        </div>
102
103        <div
104          *ngIf="showTraceCollectionConfig()"
105          class="trace-collection-config">
106          <mat-list>
107            <mat-list-item>
108              <mat-icon matListIcon>smartphone</mat-icon>
109              <p matLine>
110                {{ connect.selectedDevice()?.model }} ({{ connect.selectedDeviceId() }})
111
112                <button
113                  color="primary"
114                  class="change-btn"
115                  mat-button
116                  (click)="onChangeDeviceButton()"
117                  [disabled]="connect.isEndTraceState() || isOperationInProgress()">
118                  Change device
119                </button>
120              </p>
121            </mat-list-item>
122          </mat-list>
123
124          <mat-tab-group [selectedIndex]="selectedTabIndex" class="tracing-tabs">
125            <mat-tab
126              label="Trace"
127              [disabled]="connect.isEndTraceState() || isOperationInProgress() || refreshDumps">
128              <div class="tabbed-section">
129                <div class="trace-section" *ngIf="connect.isStartTraceState()">
130                  <trace-config [(traceConfig)]="traceConfig"></trace-config>
131                  <div class="start-btn">
132                    <button color="primary" mat-stroked-button (click)="startTracing()">
133                      Start trace
134                    </button>
135                  </div>
136                </div>
137
138                <div *ngIf="connect.isStartingTraceState()" class="starting-trace">
139                  <load-progress
140                    message="Starting trace...">
141                  </load-progress>
142                  <div class="end-btn">
143                    <button color="primary" mat-raised-button [disabled]="true">
144                      End trace
145                    </button>
146                  </div>
147                </div>
148
149                <div *ngIf="connect.isEndTraceState()" class="end-tracing">
150                  <load-progress
151                    icon="cable"
152                    message="Tracing...">
153                  </load-progress>
154                  <div class="end-btn">
155                    <button color="primary" mat-raised-button (click)="endTrace()">
156                      End trace
157                    </button>
158                  </div>
159                </div>
160
161                <div *ngIf="isOperationInProgress()" class="load-data">
162                  <load-progress
163                    [progressPercentage]="progressPercentage"
164                    [message]="progressMessage">
165                  </load-progress>
166                  <div class="end-btn">
167                    <button color="primary" mat-raised-button [disabled]="true">
168                      End trace
169                    </button>
170                  </div>
171                </div>
172              </div>
173            </mat-tab>
174            <mat-tab label="Dump" [disabled]="connect.isEndTraceState() || isOperationInProgress()">
175              <div class="tabbed-section">
176                <div class="dump-section" *ngIf="connect.isStartTraceState() && !refreshDumps">
177                  <h3 class="mat-subheading-2">Dump targets</h3>
178                  <div class="selection">
179                    <mat-checkbox
180                      *ngFor="let dumpKey of objectKeys(dumpConfig)"
181                      color="primary"
182                      class="dump-checkbox"
183                      [(ngModel)]="dumpConfig[dumpKey].run"
184                      >{{ dumpConfig[dumpKey].name }}</mat-checkbox
185                    >
186                  </div>
187                  <div class="dump-btn" *ngIf="!refreshDumps">
188                    <button color="primary" mat-stroked-button (click)="dumpState()">
189                      Dump state
190                    </button>
191                  </div>
192                </div>
193
194                <load-progress
195                  *ngIf="refreshDumps || isOperationInProgress()"
196                  [progressPercentage]="progressPercentage"
197                  [message]="progressMessage">
198                </load-progress>
199              </div>
200            </mat-tab>
201          </mat-tab-group>
202        </div>
203
204        <div *ngIf="connect.isErrorState()" class="unknown-error">
205          <p class="error-wrapper mat-body-1">
206            <mat-icon class="error-icon">error</mat-icon>
207            Error:
208          </p>
209          <pre> {{ connect.proxy?.errorText }} </pre>
210          <button color="primary" class="retry-btn" mat-raised-button (click)="onRetryButton()">
211            Retry
212          </button>
213        </div>
214      </mat-card-content>
215    </mat-card>
216  `,
217  styles: [
218    `
219      .change-btn,
220      .retry-btn {
221        margin-left: 5px;
222      }
223      .mat-card.collect-card {
224        display: flex;
225      }
226      .collect-card {
227        height: 100%;
228        flex-direction: column;
229        overflow: auto;
230        margin: 10px;
231      }
232      .collect-card-content {
233        overflow: auto;
234      }
235      .selection {
236        display: flex;
237        flex-direction: row;
238        flex-wrap: wrap;
239        gap: 10px;
240      }
241      .set-up-adb,
242      .trace-collection-config,
243      .trace-section,
244      .dump-section,
245      .starting-trace,
246      .end-tracing,
247      .load-data,
248      trace-config {
249        display: flex;
250        flex-direction: column;
251        gap: 10px;
252      }
253      .trace-section,
254      .dump-section,
255      .starting-trace,
256      .end-tracing,
257      .load-data {
258        height: 100%;
259      }
260      .trace-collection-config {
261        height: 100%;
262      }
263      .proxy-tab,
264      .web-tab,
265      .start-btn,
266      .dump-btn,
267      .end-btn {
268        align-self: flex-start;
269      }
270      .start-btn,
271      .dump-btn,
272      .end-btn {
273        margin: auto 0 0 0;
274        padding: 1rem 0 0 0;
275      }
276      .error-wrapper {
277        display: flex;
278        flex-direction: row;
279        align-items: center;
280      }
281      .error-icon {
282        margin-right: 5px;
283      }
284      .available-device {
285        cursor: pointer;
286      }
287
288      .no-device-detected {
289        display: flex;
290        flex-direction: column;
291        justify-content: center;
292        align-content: center;
293        align-items: center;
294        height: 100%;
295      }
296
297      .no-device-detected p,
298      .device-selection p.instruction {
299        padding-top: 1rem;
300        opacity: 0.6;
301        font-size: 1.2rem;
302      }
303
304      .no-device-detected .icon {
305        font-size: 3rem;
306        margin: 0 0 0.2rem 0;
307      }
308
309      .devices-connecting {
310        height: 100%;
311      }
312
313      mat-card-content {
314        flex-grow: 1;
315      }
316
317      mat-tab-body {
318        padding: 1rem;
319      }
320
321      .loading-info {
322        opacity: 0.8;
323        padding: 1rem 0;
324      }
325
326      .tracing-tabs {
327        flex-grow: 1;
328      }
329
330      .tracing-tabs .mat-tab-body-wrapper {
331        flex-grow: 1;
332      }
333
334      .tabbed-section {
335        height: 100%;
336      }
337
338      .progress-desc {
339        display: flex;
340        height: 100%;
341        flex-direction: column;
342        justify-content: center;
343        align-content: center;
344        align-items: center;
345      }
346
347      .progress-desc > * {
348        max-width: 250px;
349      }
350
351      load-progress {
352        height: 100%;
353      }
354    `,
355  ],
356  encapsulation: ViewEncapsulation.None,
357})
358export class CollectTracesComponent
359  implements OnInit, OnDestroy, ProgressListener, WinscopeEventListener
360{
361  objectKeys = Object.keys;
362  isAdbProxy = true;
363  connect: Connection | undefined;
364  isExternalOperationInProgress = false;
365  progressMessage = 'Fetching...';
366  progressPercentage: number | undefined;
367  lastUiProgressUpdateTimeMs?: number;
368  refreshDumps = false;
369  selectedTabIndex = 0;
370
371  @Input() traceConfig: TraceConfigurationMap | undefined;
372  @Input() dumpConfig: TraceConfigurationMap | undefined;
373  @Input() storage: Storage | undefined;
374
375  @Output() readonly filesCollected = new EventEmitter<File[]>();
376
377  constructor(
378    @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef,
379  ) {}
380
381  ngOnInit() {
382    if (this.isAdbProxy) {
383      this.connect = new ProxyConnection(
384        (newState) => this.onProxyStateChange(),
385        (progress) => this.onLoadProgressUpdate(progress),
386        this.setTraceConfigForAvailableTraces,
387      );
388    } else {
389      // TODO: change to WebAdbConnection
390      this.connect = new ProxyConnection(
391        (newState) => this.onProxyStateChange(),
392        (progress) => this.onLoadProgressUpdate(progress),
393        this.setTraceConfigForAvailableTraces,
394      );
395    }
396  }
397
398  ngOnDestroy(): void {
399    assertDefined(this.connect).proxy?.removeOnProxyChange(this.onProxyChange);
400  }
401
402  async onDeviceClick(deviceId: string) {
403    await assertDefined(this.connect).selectDevice(deviceId);
404  }
405
406  async onWinscopeEvent(event: WinscopeEvent) {
407    await event.visit(
408      WinscopeEventType.APP_REFRESH_DUMPS_REQUEST,
409      async (event) => {
410        this.selectedTabIndex = 1;
411        this.progressMessage = 'Refreshing dumps...';
412        this.progressPercentage = 0;
413        this.refreshDumps = true;
414      },
415    );
416  }
417
418  onProgressUpdate(message: string, progressPercentage: number | undefined) {
419    if (
420      !LoadProgressComponent.canUpdateComponent(this.lastUiProgressUpdateTimeMs)
421    ) {
422      return;
423    }
424    this.isExternalOperationInProgress = true;
425    this.progressMessage = message;
426    this.progressPercentage = progressPercentage;
427    this.lastUiProgressUpdateTimeMs = Date.now();
428    this.changeDetectorRef.detectChanges();
429  }
430
431  onOperationFinished() {
432    this.isExternalOperationInProgress = false;
433    this.lastUiProgressUpdateTimeMs = undefined;
434    this.changeDetectorRef.detectChanges();
435  }
436
437  isOperationInProgress(): boolean {
438    return (
439      assertDefined(this.connect).isLoadDataState() ||
440      this.isExternalOperationInProgress
441    );
442  }
443
444  async onAddKey(key: string) {
445    if (this.connect?.setProxyKey) {
446      this.connect.setProxyKey(key);
447    }
448    await assertDefined(this.connect).restart();
449  }
450
451  displayAdbProxyTab() {
452    this.isAdbProxy = true;
453    this.connect = new ProxyConnection(
454      (newState) => this.onProxyStateChange(),
455      (progress) => this.onLoadProgressUpdate(progress),
456      this.setTraceConfigForAvailableTraces,
457    );
458  }
459
460  displayWebAdbTab() {
461    this.isAdbProxy = false;
462    //TODO: change to WebAdbConnection
463    this.connect = new ProxyConnection(
464      (newState) => this.onProxyStateChange(),
465      (progress) => this.onLoadProgressUpdate(progress),
466      this.setTraceConfigForAvailableTraces,
467    );
468  }
469
470  showTraceCollectionConfig() {
471    const connect = assertDefined(this.connect);
472    return (
473      connect.isStartTraceState() ||
474      connect.isStartingTraceState() ||
475      connect.isEndTraceState() ||
476      this.isOperationInProgress()
477    );
478  }
479
480  async onChangeDeviceButton() {
481    await assertDefined(this.connect).resetLastDevice();
482  }
483
484  async onRetryButton() {
485    await assertDefined(this.connect).restart();
486  }
487
488  async startTracing() {
489    console.log('begin tracing');
490    const requestedTraces = this.getRequestedTraces();
491    Analytics.Tracing.logCollectTraces(requestedTraces);
492    const reqEnableConfig = this.requestedEnableConfig();
493    const reqSelectedSfConfig = this.requestedSelection('layers_trace');
494    const reqSelectedWmConfig = this.requestedSelection('window_trace');
495    if (requestedTraces.length < 1) {
496      await assertDefined(this.connect).throwNoTargetsError();
497      return;
498    }
499
500    await assertDefined(this.connect).startTrace(
501      requestedTraces,
502      reqEnableConfig,
503      reqSelectedSfConfig,
504      reqSelectedWmConfig,
505    );
506  }
507
508  async dumpState() {
509    console.log('begin dump');
510    const requestedDumps = this.getRequestedDumps();
511    Analytics.Tracing.logCollectDumps(requestedDumps);
512    const dumpSuccessful = await assertDefined(this.connect).dumpState(
513      requestedDumps,
514    );
515    this.refreshDumps = false;
516    if (dumpSuccessful) {
517      this.filesCollected.emit(assertDefined(this.connect).adbData());
518    }
519  }
520
521  async endTrace() {
522    console.log('end tracing');
523    await assertDefined(this.connect).endTrace();
524    this.filesCollected.emit(assertDefined(this.connect).adbData());
525  }
526
527  tabClass(adbTab: boolean) {
528    let isActive: string;
529    if (adbTab) {
530      isActive = this.isAdbProxy ? 'active' : 'inactive';
531    } else {
532      isActive = !this.isAdbProxy ? 'active' : 'inactive';
533    }
534    return ['tab', isActive];
535  }
536
537  private async onProxyChange(newState: ProxyState) {
538    await assertDefined(this.connect).onConnectChange.bind(this.connect)(
539      newState,
540    );
541  }
542
543  private onProxyStateChange() {
544    this.changeDetectorRef.detectChanges();
545    if (
546      !this.refreshDumps ||
547      this.connect?.isLoadDataState() ||
548      this.connect?.isConnectingState()
549    ) {
550      return;
551    }
552    if (this.connect?.isStartTraceState()) {
553      this.dumpState();
554    } else {
555      // device is not connected or proxy is not started/invalid/in error state
556      // so cannot refresh dump automatically
557      this.refreshDumps = false;
558    }
559  }
560
561  private getRequestedTraces() {
562    const tracesFromCollection: string[] = [];
563    const tracingConfig = assertDefined(this.traceConfig);
564    const requested = Object.keys(tracingConfig).filter((traceKey: string) => {
565      return tracingConfig[traceKey].run;
566    });
567    requested.push(...tracesFromCollection);
568    requested.push('perfetto_trace'); // always start/stop/fetch perfetto trace
569    return requested;
570  }
571
572  private getRequestedDumps() {
573    const dumpConfig = assertDefined(this.dumpConfig);
574    const requested = Object.keys(dumpConfig).filter((dumpKey: string) => {
575      return dumpConfig[dumpKey].run;
576    });
577    requested.push('perfetto_dump'); // always dump/fetch perfetto dump
578    return requested;
579  }
580
581  private requestedEnableConfig(): string[] {
582    const req: string[] = [];
583    const tracingConfig = assertDefined(this.traceConfig);
584    Object.keys(tracingConfig).forEach((traceKey: string) => {
585      const trace = tracingConfig[traceKey];
586      if (trace.run && trace.config && trace.config.enableConfigs) {
587        trace.config.enableConfigs.forEach((con: EnableConfiguration) => {
588          if (con.enabled) {
589            req.push(con.key);
590          }
591        });
592      }
593    });
594    return req;
595  }
596
597  private requestedSelection(traceType: string): ConfigMap | undefined {
598    const tracingConfig = assertDefined(this.traceConfig);
599    if (!tracingConfig[traceType].run) {
600      return undefined;
601    }
602    const selected: ConfigMap = {};
603    tracingConfig[traceType].config?.selectionConfigs.forEach(
604      (con: SelectionConfiguration) => {
605        selected[con.key] = con.value;
606      },
607    );
608    return selected;
609  }
610
611  private onLoadProgressUpdate(progressPercentage: number) {
612    this.progressPercentage = progressPercentage;
613    this.changeDetectorRef.detectChanges();
614  }
615
616  private setTraceConfigForAvailableTraces = (
617    availableTracesConfig: TraceConfigurationMap,
618  ) =>
619    (this.traceConfig = PersistentStoreProxy.new<TraceConfigurationMap>(
620      'TraceConfiguration',
621      availableTracesConfig,
622      assertDefined(this.storage),
623    ));
624}
625