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