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