• 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  Inject,
21  Injector,
22  ViewChild,
23  ViewEncapsulation,
24} from '@angular/core';
25import {createCustomElement} from '@angular/elements';
26import {AbtChromeExtensionProtocol} from 'abt_chrome_extension/abt_chrome_extension_protocol';
27import {Mediator} from 'app/mediator';
28import {TimelineData} from 'app/timeline_data';
29import {TRACE_INFO} from 'app/trace_info';
30import {TracePipeline} from 'app/trace_pipeline';
31import {FileUtils} from 'common/file_utils';
32import {PersistentStore} from 'common/persistent_store';
33import {CrossToolProtocol} from 'cross_tool/cross_tool_protocol';
34import {TraceDataListener} from 'interfaces/trace_data_listener';
35import {Timestamp} from 'trace/timestamp';
36import {Trace} from 'trace/trace';
37import {TraceType} from 'trace/trace_type';
38import {proxyClient, ProxyState} from 'trace_collection/proxy_client';
39import {ViewerInputMethodComponent} from 'viewers/components/viewer_input_method_component';
40import {View, Viewer} from 'viewers/viewer';
41import {ViewerProtologComponent} from 'viewers/viewer_protolog/viewer_protolog_component';
42import {ViewerScreenRecordingComponent} from 'viewers/viewer_screen_recording/viewer_screen_recording_component';
43import {ViewerSurfaceFlingerComponent} from 'viewers/viewer_surface_flinger/viewer_surface_flinger_component';
44import {ViewerTransactionsComponent} from 'viewers/viewer_transactions/viewer_transactions_component';
45import {ViewerTransitionsComponent} from 'viewers/viewer_transitions/viewer_transitions_component';
46import {ViewerViewCaptureComponent} from 'viewers/viewer_view_capture/viewer_view_capture_component';
47import {ViewerWindowManagerComponent} from 'viewers/viewer_window_manager/viewer_window_manager_component';
48import {CollectTracesComponent} from './collect_traces_component';
49import {SnackBarOpener} from './snack_bar_opener';
50import {TimelineComponent} from './timeline/timeline_component';
51import {UploadTracesComponent} from './upload_traces_component';
52
53@Component({
54  selector: 'app-root',
55  template: `
56    <mat-toolbar class="toolbar">
57      <span class="app-title">Winscope</span>
58
59      <a href="http://go/winscope-legacy">
60        <button color="primary" mat-button>Open legacy Winscope</button>
61      </a>
62
63      <div class="spacer">
64        <mat-icon
65          *ngIf="dataLoaded && activeTrace"
66          class="icon"
67          [matTooltip]="TRACE_INFO[activeTrace.type].name"
68          [style]="{color: TRACE_INFO[activeTrace.type].color, marginRight: '0.5rem'}">
69          {{ TRACE_INFO[activeTrace.type].icon }}
70        </mat-icon>
71        <span *ngIf="dataLoaded" class="active-trace-file-info mat-body-2">
72          {{ activeTraceFileInfo }}
73        </span>
74      </div>
75
76      <button
77        *ngIf="dataLoaded"
78        color="primary"
79        mat-stroked-button
80        (click)="mediator.onWinscopeUploadNew()">
81        Upload New
82      </button>
83
84      <button
85        mat-icon-button
86        matTooltip="Report bug"
87        (click)="goToLink('https://b.corp.google.com/issues/new?component=909476')">
88        <mat-icon> bug_report</mat-icon>
89      </button>
90
91      <button
92        mat-icon-button
93        matTooltip="Switch to {{ isDarkModeOn ? 'light' : 'dark' }} mode"
94        (click)="setDarkMode(!isDarkModeOn)">
95        <mat-icon>
96          {{ isDarkModeOn ? 'brightness_5' : 'brightness_4' }}
97        </mat-icon>
98      </button>
99    </mat-toolbar>
100
101    <mat-divider></mat-divider>
102
103    <mat-drawer-container class="example-container" autosize disableClose autoFocus>
104      <mat-drawer-content>
105        <ng-container *ngIf="dataLoaded; else noLoadedTracesBlock">
106          <trace-view
107            class="viewers"
108            [viewers]="viewers"
109            [store]="store"
110            (downloadTracesButtonClick)="onDownloadTracesButtonClick()"
111            (activeViewChanged)="onActiveViewChanged($event)"></trace-view>
112
113          <mat-divider></mat-divider>
114        </ng-container>
115      </mat-drawer-content>
116
117      <mat-drawer #drawer mode="overlay" opened="true" [baseHeight]="collapsedTimelineHeight">
118        <timeline
119          *ngIf="dataLoaded"
120          [timelineData]="timelineData"
121          [activeViewTraceTypes]="activeView?.dependencies"
122          [availableTraces]="getLoadedTraceTypes()"
123          (collapsedTimelineSizeChanged)="onCollapsedTimelineSizeChanged($event)"></timeline>
124      </mat-drawer>
125    </mat-drawer-container>
126
127    <ng-template #noLoadedTracesBlock>
128      <div class="center">
129        <div class="landing-content">
130          <h1 class="welcome-info mat-headline">
131            Welcome to Winscope. Please select source to view traces.
132          </h1>
133
134          <div class="card-grid landing-grid">
135            <collect-traces
136              class="collect-traces-card homepage-card"
137              (filesCollected)="mediator.onWinscopeFilesCollected($event)"
138              [store]="store"></collect-traces>
139
140            <upload-traces
141              class="upload-traces-card homepage-card"
142              [tracePipeline]="tracePipeline"
143              (filesUploaded)="mediator.onWinscopeFilesUploaded($event)"
144              (viewTracesButtonClick)="mediator.onWinscopeViewTracesRequest()"></upload-traces>
145          </div>
146        </div>
147      </div>
148    </ng-template>
149  `,
150  styles: [
151    `
152      .toolbar {
153        gap: 10px;
154      }
155      .welcome-info {
156        margin: 16px 0 6px 0;
157        text-align: center;
158      }
159      .homepage-card {
160        display: flex;
161        flex-direction: column;
162        flex: 1;
163        overflow: auto;
164        height: 820px;
165      }
166      .spacer {
167        flex: 1;
168        text-align: center;
169        display: flex;
170        align-items: center;
171        justify-content: center;
172      }
173      .viewers {
174        height: 0;
175        flex-grow: 1;
176        display: flex;
177        flex-direction: column;
178        overflow: auto;
179      }
180      .center {
181        display: flex;
182        align-content: center;
183        flex-direction: column;
184        justify-content: center;
185        align-items: center;
186        justify-items: center;
187        flex-grow: 1;
188      }
189      .landing-content {
190        width: 100%;
191      }
192      .landing-content .card-grid {
193        max-width: 1800px;
194        flex-grow: 1;
195        margin: auto;
196      }
197    `,
198  ],
199  encapsulation: ViewEncapsulation.None,
200})
201export class AppComponent implements TraceDataListener {
202  title = 'winscope';
203  changeDetectorRef: ChangeDetectorRef;
204  snackbarOpener: SnackBarOpener;
205  tracePipeline = new TracePipeline();
206  timelineData = new TimelineData();
207  abtChromeExtensionProtocol = new AbtChromeExtensionProtocol();
208  crossToolProtocol = new CrossToolProtocol();
209  mediator: Mediator;
210  states = ProxyState;
211  store: PersistentStore = new PersistentStore();
212  currentTimestamp?: Timestamp;
213  viewers: Viewer[] = [];
214  isDarkModeOn!: boolean;
215  dataLoaded = false;
216  activeView?: View;
217  activeTrace?: Trace<object>;
218  activeTraceFileInfo = '';
219  collapsedTimelineHeight = 0;
220  @ViewChild(UploadTracesComponent) uploadTracesComponent?: UploadTracesComponent;
221  @ViewChild(CollectTracesComponent) collectTracesComponent?: UploadTracesComponent;
222  @ViewChild(TimelineComponent) timelineComponent?: TimelineComponent;
223  TRACE_INFO = TRACE_INFO;
224
225  constructor(
226    @Inject(Injector) injector: Injector,
227    @Inject(ChangeDetectorRef) changeDetectorRef: ChangeDetectorRef,
228    @Inject(SnackBarOpener) snackBar: SnackBarOpener
229  ) {
230    this.changeDetectorRef = changeDetectorRef;
231    this.snackbarOpener = snackBar;
232    this.mediator = new Mediator(
233      this.tracePipeline,
234      this.timelineData,
235      this.abtChromeExtensionProtocol,
236      this.crossToolProtocol,
237      this,
238      this.snackbarOpener,
239      localStorage
240    );
241
242    const storeDarkMode = this.store.get('dark-mode');
243    const prefersDarkQuery = window.matchMedia?.('(prefers-color-scheme: dark)');
244    this.setDarkMode(storeDarkMode ? storeDarkMode === 'true' : prefersDarkQuery.matches);
245
246    if (!customElements.get('viewer-input-method')) {
247      customElements.define(
248        'viewer-input-method',
249        createCustomElement(ViewerInputMethodComponent, {injector})
250      );
251    }
252    if (!customElements.get('viewer-protolog')) {
253      customElements.define(
254        'viewer-protolog',
255        createCustomElement(ViewerProtologComponent, {injector})
256      );
257    }
258    if (!customElements.get('viewer-screen-recording')) {
259      customElements.define(
260        'viewer-screen-recording',
261        createCustomElement(ViewerScreenRecordingComponent, {injector})
262      );
263    }
264    if (!customElements.get('viewer-surface-flinger')) {
265      customElements.define(
266        'viewer-surface-flinger',
267        createCustomElement(ViewerSurfaceFlingerComponent, {injector})
268      );
269    }
270    if (!customElements.get('viewer-transactions')) {
271      customElements.define(
272        'viewer-transactions',
273        createCustomElement(ViewerTransactionsComponent, {injector})
274      );
275    }
276    if (!customElements.get('viewer-window-manager')) {
277      customElements.define(
278        'viewer-window-manager',
279        createCustomElement(ViewerWindowManagerComponent, {injector})
280      );
281    }
282    if (!customElements.get('viewer-transitions')) {
283      customElements.define(
284        'viewer-transitions',
285        createCustomElement(ViewerTransitionsComponent, {injector})
286      );
287    }
288    if (!customElements.get('viewer-view-capture')) {
289      customElements.define(
290        'viewer-view-capture',
291        createCustomElement(ViewerViewCaptureComponent, {injector})
292      );
293    }
294  }
295
296  ngAfterViewInit() {
297    this.mediator.onWinscopeInitialized();
298  }
299
300  ngAfterViewChecked() {
301    this.mediator.setUploadTracesComponent(this.uploadTracesComponent);
302    this.mediator.setCollectTracesComponent(this.collectTracesComponent);
303    this.mediator.setTimelineComponent(this.timelineComponent);
304  }
305
306  onCollapsedTimelineSizeChanged(height: number) {
307    this.collapsedTimelineHeight = height;
308    this.changeDetectorRef.detectChanges();
309  }
310
311  getLoadedTraceTypes(): TraceType[] {
312    return this.tracePipeline.getTraces().mapTrace((trace) => trace.type);
313  }
314
315  onTraceDataLoaded(viewers: Viewer[]) {
316    this.viewers = viewers;
317    this.dataLoaded = true;
318    this.changeDetectorRef.detectChanges();
319  }
320
321  onTraceDataUnloaded() {
322    proxyClient.adbData = [];
323    this.dataLoaded = false;
324    this.changeDetectorRef.detectChanges();
325  }
326
327  setDarkMode(enabled: boolean) {
328    document.body.classList.toggle('dark-mode', enabled);
329    this.store.add('dark-mode', `${enabled}`);
330    this.isDarkModeOn = enabled;
331  }
332
333  async onDownloadTracesButtonClick() {
334    const traceFiles = await this.makeTraceFilesForDownload();
335    const zipFileBlob = await FileUtils.createZipArchive(traceFiles);
336    const zipFileName = 'winscope.zip';
337
338    const a = document.createElement('a');
339    document.body.appendChild(a);
340    const url = window.URL.createObjectURL(zipFileBlob);
341    a.href = url;
342    a.download = zipFileName;
343    a.click();
344    window.URL.revokeObjectURL(url);
345    document.body.removeChild(a);
346  }
347
348  async onActiveViewChanged(view: View) {
349    this.activeView = view;
350    this.activeTrace = this.getActiveTrace(view);
351    this.activeTraceFileInfo = this.makeActiveTraceFileInfo(view);
352    await this.mediator.onWinscopeActiveViewChanged(view);
353  }
354
355  goToLink(url: string) {
356    window.open(url, '_blank');
357  }
358
359  private makeActiveTraceFileInfo(view: View): string {
360    const trace = this.getActiveTrace(view);
361
362    if (!trace) {
363      return '';
364    }
365
366    return `${trace.getDescriptors().join(', ')}`;
367  }
368
369  private getActiveTrace(view: View): Trace<object> | undefined {
370    let activeTrace: Trace<object> | undefined;
371    this.tracePipeline.getTraces().forEachTrace((trace) => {
372      if (trace.type === view.dependencies[0]) {
373        activeTrace = trace;
374      }
375    });
376    return activeTrace;
377  }
378
379  private async makeTraceFilesForDownload(): Promise<File[]> {
380    const loadedFiles = this.tracePipeline.getLoadedFiles();
381    return [...loadedFiles.keys()].map((traceType) => {
382      const file = loadedFiles.get(traceType)!;
383      const path = TRACE_INFO[traceType].downloadArchiveDir;
384
385      const newName = path + '/' + FileUtils.removeDirFromFileName(file.file.name);
386      return new File([file.file], newName);
387    });
388  }
389}
390