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