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 Output, 25 ViewEncapsulation, 26} from '@angular/core'; 27import {MatDialog} from '@angular/material/dialog'; 28import {MatSelectChange} from '@angular/material/select'; 29import { 30 assertDefined, 31 assertTrue, 32 assertUnreachable, 33} from 'common/assert_utils'; 34import {FunctionUtils} from 'common/function_utils'; 35import {Store} from 'common/store/store'; 36import {UserNotifier} from 'common/user_notifier'; 37import {Analytics} from 'logging/analytics'; 38import {ProgressListener} from 'messaging/progress_listener'; 39import {ProxyTraceTimeout} from 'messaging/user_warnings'; 40import { 41 NoTraceTargetsSelected, 42 WinscopeEvent, 43 WinscopeEventType, 44} from 'messaging/winscope_event'; 45import { 46 EmitEvent, 47 WinscopeEventEmitter, 48} from 'messaging/winscope_event_emitter'; 49import {WinscopeEventListener} from 'messaging/winscope_event_listener'; 50import { 51 AdbDeviceConnection, 52 AdbDeviceState, 53} from 'trace_collection/adb/adb_device_connection'; 54import {AdbConnectionType} from 'trace_collection/adb_connection_type'; 55import {AdbFiles, RequestedTraceTypes} from 'trace_collection/adb_files'; 56import {ConnectionState} from 'trace_collection/connection_state'; 57import {ConnectionStateListener} from 'trace_collection/connection_state_listener'; 58import {TraceCollectionController} from 'trace_collection/controller/trace_collection_controller'; 59import { 60 CheckboxConfiguration, 61 makeDefaultDumpConfigMap, 62 makeDefaultTraceConfigMap, 63 makeScreenRecordingSelectionConfigs, 64 SelectionConfiguration, 65 TraceConfigurationMap, 66 updateConfigsFromStore, 67} from 'trace_collection/ui/ui_trace_configuration'; 68import {UiTraceTarget} from 'trace_collection/ui/ui_trace_target'; 69import {UserRequest, UserRequestConfig} from 'trace_collection/user_request'; 70import {LoadProgressComponent} from './load_progress_component'; 71import { 72 WarningDialogComponent, 73 WarningDialogData, 74 WarningDialogResult, 75} from './warning_dialog_component'; 76 77@Component({ 78 selector: 'collect-traces', 79 template: ` 80 <mat-card class="collect-card"> 81 <mat-card-title class="title">Collect Traces</mat-card-title> 82 83 <mat-card-content *ngIf="controller" class="collect-card-content"> 84 <mat-form-field class="connection-type"> 85 <mat-label>Select connection type</mat-label> 86 <mat-select 87 [value]="getConnectionType()" 88 (selectionChange)="onConnectionChange($event)" 89 [disabled]="disableTraceSection()"> 90 <mat-option [value]="AdbConnectionType.WINSCOPE_PROXY"> 91 <span>{{AdbConnectionType.WINSCOPE_PROXY}}</span> 92 </mat-option> 93 <mat-option [value]="AdbConnectionType.WDP"> 94 <span>{{AdbConnectionType.WDP}}</span> 95 </mat-option> 96 </mat-select> 97 </mat-form-field> 98 99 <button 100 mat-icon-button 101 class="refresh-connection" 102 (click)="onRetryConnection()" 103 matTooltip="Refresh connection"><mat-icon>refresh</mat-icon></button> 104 105 <ng-container *ngIf="!adbSuccess()"> 106 <winscope-proxy-setup 107 *ngIf="getConnectionType() === AdbConnectionType.WINSCOPE_PROXY" 108 [state]="state" 109 (retryConnection)="onRetryConnection($event)"></winscope-proxy-setup> 110 <wdp-setup 111 *ngIf="getConnectionType() === AdbConnectionType.WDP" 112 [state]="state" 113 (retryConnection)="onRetryConnection()"></wdp-setup> 114 </ng-container> 115 116 <div *ngIf="showAllDevices()" class="devices-connecting"> 117 <div 118 *ngIf="controller.getDevices().length === 0" 119 class="no-device-detected"> 120 <p class="mat-body-3 icon"> 121 <mat-icon inline fontIcon="phonelink_erase"></mat-icon> 122 </p> 123 <p class="mat-body-1">No devices detected</p> 124 </div> 125 <div 126 *ngIf="controller.getDevices().length > 0" 127 class="device-selection"> 128 <p class="mat-body-1 instruction">Select a device:</p> 129 <mat-list> 130 <mat-list-item 131 *ngFor="let device of controller.getDevices()" 132 [disabled]="device.state === ${AdbDeviceState.OFFLINE}" 133 (click)="onDeviceClick(device)" 134 class="available-device"> 135 <mat-icon matListIcon> 136 {{ getDeviceStateIcon(device.state) }} 137 </mat-icon> 138 <p matLine> 139 {{ getDeviceName(device) }} 140 </p> 141 <mat-icon 142 *ngIf="showTryAuthorizeButton(device)" 143 class="material-symbols-outlined authorize-btn" 144 matTooltip="Authorize device" 145 (click)="device.tryAuthorize()">lock_open</mat-icon> 146 </mat-list-item> 147 </mat-list> 148 </div> 149 </div> 150 151 <div 152 *ngIf="showTraceCollectionConfig()" 153 class="trace-collection-config"> 154 <mat-list> 155 <mat-list-item class="selected-device"> 156 <mat-icon matListIcon>smartphone</mat-icon> 157 <p matLine> 158 {{ getSelectedDevice()}} 159 </p> 160 161 <div class="device-actions"> 162 <button 163 color="primary" 164 class="change-btn" 165 mat-stroked-button 166 (click)="onChangeDeviceButton()" 167 [disabled]="isTracingOrLoading()"> 168 Change device 169 </button> 170 <button 171 color="primary" 172 class="fetch-btn" 173 mat-stroked-button 174 (click)="fetchExistingTraces()" 175 [disabled]="isTracingOrLoading()"> 176 Fetch traces from last session 177 </button> 178 </div> 179 </mat-list-item> 180 </mat-list> 181 182 <mat-tab-group [selectedIndex]="targetTabIndex" class="target-tabs"> 183 <mat-tab 184 label="Trace" 185 [disabled]="disableTraceSection()"> 186 <div class="tabbed-section"> 187 <div 188 class="trace-section" 189 *ngIf="state === ${ConnectionState.IDLE}"> 190 <trace-config 191 title="Trace targets" 192 [traceConfig]="traceConfig" 193 [storage]="storage" 194 [traceConfigStoreKey]="storeKeyPrefixTraceConfig" 195 (traceConfigChange)="onTraceConfigChange($event)"></trace-config> 196 <div class="start-btn"> 197 <button 198 color="primary" 199 mat-raised-button 200 (click)="startTracing()">Start trace</button> 201 </div> 202 </div> 203 204 <div *ngIf="isTracingOrLoading()" class="tracing-progress"> 205 <load-progress 206 [icon]="progressIcon" 207 [message]="progressMessage" 208 [progressPercentage]="progressPercentage"> 209 </load-progress> 210 <div class="end-btn" *ngIf="isTracing()"> 211 <button 212 color="primary" 213 mat-raised-button 214 [disabled]="state !== ${ConnectionState.TRACING}" 215 (click)="endTrace()"> 216 End trace 217 </button> 218 </div> 219 </div> 220 </div> 221 </mat-tab> 222 <mat-tab 223 label="Dump" 224 [disabled]="isTracingOrLoading()"> 225 <div class="tabbed-section"> 226 <div 227 class="dump-section" 228 *ngIf="state === ${ConnectionState.IDLE} && !refreshDumps"> 229 <trace-config 230 title="Dump targets" 231 [traceConfig]="dumpConfig" 232 [storage]="storage" 233 [traceConfigStoreKey]="storeKeyPrefixDumpConfig" 234 (traceConfigChange)="onDumpConfigChange($event)"></trace-config> 235 <div class="dump-btn" *ngIf="!refreshDumps"> 236 <button 237 color="primary" 238 mat-raised-button 239 (click)="dumpState()">Dump state</button> 240 </div> 241 </div> 242 243 <load-progress 244 class="dumping-state" 245 *ngIf="isDumpingState()" 246 [progressPercentage]="progressPercentage" 247 [message]="progressMessage"> 248 </load-progress> 249 </div> 250 </mat-tab> 251 </mat-tab-group> 252 </div> 253 254 <div *ngIf="state === ${ConnectionState.ERROR}" class="unknown-error"> 255 <p class="error-wrapper mat-body-1"> 256 <mat-icon class="error-icon">error</mat-icon> 257 Error: 258 </p> 259 <pre> {{ errorText }} </pre> 260 <button 261 color="primary" 262 class="retry-btn" 263 mat-raised-button 264 (click)="onRetryButton()">Retry</button> 265 </div> 266 </mat-card-content> 267 </mat-card> 268 `, 269 styles: [ 270 ` 271 .change-btn, 272 .retry-btn, 273 .fetch-btn { 274 margin-left: 5px; 275 } 276 .fetch-btn { 277 margin-top: 5px; 278 } 279 .selected-device { 280 height: fit-content !important; 281 } 282 .mat-card.collect-card { 283 display: flex; 284 } 285 .collect-card { 286 height: 100%; 287 flex-direction: column; 288 overflow: auto; 289 margin: 10px; 290 } 291 .collect-card-content { 292 overflow: auto; 293 } 294 .selection { 295 display: flex; 296 flex-direction: row; 297 flex-wrap: wrap; 298 gap: 10px; 299 } 300 .trace-collection-config, 301 .trace-section, 302 .dump-section, 303 .tracing-progress, 304 trace-config { 305 display: flex; 306 flex-direction: column; 307 gap: 10px; 308 } 309 .trace-section, 310 .dump-section, 311 .tracing-progress { 312 height: 100%; 313 } 314 .winscope-proxy-setup-tab, 315 .web-tab, 316 .start-btn, 317 .dump-btn, 318 .end-btn { 319 align-self: flex-start; 320 } 321 .start-btn, 322 .dump-btn, 323 .end-btn { 324 margin: auto 0 0 0; 325 padding: 1rem 0 0 0; 326 } 327 .error-wrapper { 328 display: flex; 329 flex-direction: row; 330 align-items: center; 331 } 332 .error-icon { 333 margin-right: 5px; 334 } 335 .available-device { 336 cursor: pointer; 337 } 338 339 .no-device-detected { 340 display: flex; 341 flex-direction: column; 342 justify-content: center; 343 align-content: center; 344 align-items: center; 345 height: 100%; 346 } 347 348 .no-device-detected p, 349 .device-selection p.instruction { 350 padding-top: 1rem; 351 opacity: 0.6; 352 font-size: 1.2rem; 353 } 354 355 .no-device-detected .icon { 356 font-size: 3rem; 357 margin: 0 0 0.2rem 0; 358 } 359 360 mat-card-content { 361 flex-grow: 1; 362 } 363 364 mat-tab-body { 365 padding: 1rem; 366 } 367 368 .loading-info { 369 opacity: 0.8; 370 padding: 1rem 0; 371 } 372 373 .target-tabs { 374 flex-grow: 1; 375 } 376 377 .target-tabs .mat-tab-body-wrapper { 378 flex-grow: 1; 379 } 380 381 .tabbed-section { 382 height: 100%; 383 } 384 385 .progress-desc { 386 display: flex; 387 height: 100%; 388 flex-direction: column; 389 justify-content: center; 390 align-content: center; 391 align-items: center; 392 } 393 394 .progress-desc > * { 395 max-width: 250px; 396 } 397 398 load-progress { 399 height: 100%; 400 } 401 `, 402 ], 403 encapsulation: ViewEncapsulation.None, 404}) 405export class CollectTracesComponent 406 implements 407 ProgressListener, 408 WinscopeEventListener, 409 WinscopeEventEmitter, 410 ConnectionStateListener 411{ 412 objectKeys = Object.keys; 413 AdbConnectionType = AdbConnectionType; 414 isExternalOperationInProgress = false; 415 progressMessage = 'Fetching...'; 416 progressIcon = 'sync'; 417 progressPercentage: number | undefined; 418 lastUiProgressUpdateTimeMs?: number; 419 refreshDumps = false; 420 targetTabIndex = 0; 421 traceConfig: TraceConfigurationMap; 422 dumpConfig: TraceConfigurationMap; 423 requestedTraceTypes: RequestedTraceTypes[] = []; 424 controller: TraceCollectionController | undefined; 425 state = ConnectionState.CONNECTING; 426 errorText = ''; 427 428 readonly storeKeyPrefixTraceConfig = 'TraceSettings.'; 429 readonly storeKeyPrefixDumpConfig = 'DumpSettings.'; 430 private readonly storeKeyImeWarning = 'doNotShowImeWarningDialog'; 431 private readonly storeKeyLastDevice = 'adb.lastDevice'; 432 private readonly storeKeyAdbConnectionType = 'adbConnectionType'; 433 434 private selectedDevice: AdbDeviceConnection | undefined; 435 private emitEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC; 436 437 private readonly notConnected = [ 438 ConnectionState.CONNECTING, 439 ConnectionState.NOT_FOUND, 440 ConnectionState.UNAUTH, 441 ConnectionState.INVALID_VERSION, 442 ]; 443 private readonly tracingSessionStates = [ 444 ConnectionState.STARTING_TRACE, 445 ConnectionState.TRACING, 446 ConnectionState.ENDING_TRACE, 447 ConnectionState.DUMPING_STATE, 448 ]; 449 450 @Input() storage: Store | undefined; 451 @Output() readonly filesCollected = new EventEmitter<AdbFiles>(); 452 453 constructor( 454 @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef, 455 @Inject(MatDialog) private dialog: MatDialog, 456 @Inject(NgZone) private ngZone: NgZone, 457 ) { 458 this.traceConfig = makeDefaultTraceConfigMap(); 459 this.dumpConfig = makeDefaultDumpConfigMap(); 460 } 461 462 async ngOnInit() { 463 const adbConnectionType = this.storage?.get(this.storeKeyAdbConnectionType); 464 if (adbConnectionType !== undefined) { 465 await this.changeHostConnection(adbConnectionType); 466 } else { 467 await this.changeHostConnection(AdbConnectionType.WINSCOPE_PROXY); 468 } 469 } 470 471 getConnectionType() { 472 return this.controller?.getConnectionType(); 473 } 474 475 ngOnDestroy() { 476 if (this.selectedDevice) { 477 this.controller?.onDestroy(this.selectedDevice); 478 } 479 } 480 481 setEmitEvent(callback: EmitEvent) { 482 this.emitEvent = callback; 483 } 484 485 async onConnectionChange(event: MatSelectChange) { 486 this.changeHostConnection(event.value); 487 } 488 489 onDeviceClick(device: AdbDeviceConnection) { 490 this.selectedDevice = device; 491 this.onDevicesChange(assertDefined(this.controller).getDevices()); 492 this.storage?.add(this.storeKeyLastDevice, device.id); 493 this.changeDetectorRef.detectChanges(); 494 } 495 496 async onWinscopeEvent(event: WinscopeEvent) { 497 await event.visit( 498 WinscopeEventType.APP_REFRESH_DUMPS_REQUEST, 499 async (event) => { 500 this.targetTabIndex = 1; 501 this.dumpConfig = updateConfigsFromStore( 502 JSON.parse(JSON.stringify(assertDefined(this.dumpConfig))), 503 assertDefined(this.storage), 504 this.storeKeyPrefixDumpConfig, 505 ); 506 this.refreshDumps = true; 507 }, 508 ); 509 } 510 511 onProgressUpdate(message: string, progressPercentage: number | undefined) { 512 if ( 513 !LoadProgressComponent.canUpdateComponent(this.lastUiProgressUpdateTimeMs) 514 ) { 515 return; 516 } 517 this.isExternalOperationInProgress = true; 518 this.progressMessage = message; 519 this.progressPercentage = progressPercentage; 520 this.lastUiProgressUpdateTimeMs = Date.now(); 521 this.changeDetectorRef.detectChanges(); 522 } 523 524 onOperationFinished(success: boolean) { 525 this.isExternalOperationInProgress = false; 526 this.lastUiProgressUpdateTimeMs = undefined; 527 if (!success) { 528 this.controller?.restartConnection(); 529 } 530 this.changeDetectorRef.detectChanges(); 531 } 532 533 isLoadOperationInProgress(): boolean { 534 return ( 535 this.state === ConnectionState.LOADING_DATA || 536 this.isExternalOperationInProgress 537 ); 538 } 539 540 async onRetryConnection(token?: string) { 541 const controller = assertDefined(this.controller); 542 if (token !== undefined) { 543 controller.setSecurityToken(token); 544 } 545 await controller.restartConnection(); 546 } 547 548 showAllDevices(): boolean { 549 const controller = assertDefined(this.controller); 550 if (this.state !== ConnectionState.IDLE) { 551 return false; 552 } 553 554 const devices = controller.getDevices(); 555 const lastId = this.storage?.get(this.storeKeyLastDevice) ?? undefined; 556 557 if (this.selectedDevice) { 558 const newDevice = devices.find((d) => d.id === this.selectedDevice?.id); 559 if (newDevice && newDevice.getState() === AdbDeviceState.AVAILABLE) { 560 this.selectedDevice = newDevice; 561 } else { 562 this.selectedDevice = undefined; 563 } 564 } 565 566 if (this.selectedDevice === undefined && lastId !== undefined) { 567 const device = devices.find((d) => d.id === lastId); 568 if (device && device.getState() === AdbDeviceState.AVAILABLE) { 569 this.selectedDevice = device; 570 this.onDevicesChange(devices); 571 this.storage?.add(this.storeKeyLastDevice, device.id); 572 return false; 573 } 574 } 575 576 return this.selectedDevice === undefined; 577 } 578 579 showTraceCollectionConfig(): boolean { 580 if (this.selectedDevice === undefined) { 581 return false; 582 } 583 return this.state === ConnectionState.IDLE || this.isTracingOrLoading(); 584 } 585 586 onTraceConfigChange(newConfig: TraceConfigurationMap) { 587 this.traceConfig = newConfig; 588 } 589 590 onDumpConfigChange(newConfig: TraceConfigurationMap) { 591 this.dumpConfig = newConfig; 592 } 593 594 async onChangeDeviceButton() { 595 this.storage?.add(this.storeKeyLastDevice, ''); 596 this.selectedDevice = undefined; 597 await this.controller?.restartConnection(); 598 } 599 600 async onRetryButton() { 601 await assertDefined(this.controller).restartConnection(); 602 } 603 604 adbSuccess() { 605 return !this.notConnected.includes(this.state); 606 } 607 608 async startTracing() { 609 const requestedTraces = this.getRequests(assertDefined(this.traceConfig)); 610 const imeReq = requestedTraces.includes(UiTraceTarget.IME); 611 const doNotShowDialog = !!this.storage?.get(this.storeKeyImeWarning); 612 613 if (!imeReq || doNotShowDialog) { 614 await this.requestTraces(requestedTraces); 615 return; 616 } 617 618 const sfReq = requestedTraces.includes(UiTraceTarget.SURFACE_FLINGER_TRACE); 619 const transactionsReq = requestedTraces.includes( 620 UiTraceTarget.TRANSACTIONS, 621 ); 622 const wmReq = requestedTraces.includes(UiTraceTarget.WINDOW_MANAGER_TRACE); 623 const imeValidFrameMapping = sfReq && transactionsReq && wmReq; 624 625 if (imeValidFrameMapping) { 626 await this.requestTraces(requestedTraces); 627 return; 628 } 629 630 this.ngZone.run(() => { 631 const closeText = 'Collect traces anyway'; 632 const optionText = 'Do not show again'; 633 const data: WarningDialogData = { 634 message: `Cannot build frame mapping for IME with selected traces - some Winscope features may not work properly. 635 Consider the following selection for valid frame mapping: 636 Surface Flinger, Transactions, Window Manager, IME`, 637 actions: ['Go back'], 638 options: [optionText], 639 closeText, 640 }; 641 const dialogRef = this.dialog.open(WarningDialogComponent, { 642 data, 643 disableClose: true, 644 }); 645 dialogRef 646 .beforeClosed() 647 .subscribe((result: WarningDialogResult | undefined) => { 648 if (this.storage && result?.selectedOptions.includes(optionText)) { 649 this.storage.add(this.storeKeyImeWarning, 'true'); 650 } 651 if (result?.closeActionText === closeText) { 652 this.requestTraces(requestedTraces); 653 } 654 }); 655 }); 656 } 657 658 async dumpState() { 659 const requestedDumps = this.getRequests(assertDefined(this.dumpConfig)); 660 if (requestedDumps.length === 0) { 661 this.emitEvent(new NoTraceTargetsSelected()); 662 return; 663 } 664 665 const requestedTraceTypes = requestedDumps.map((req) => { 666 return { 667 name: this.dumpConfig[req].name, 668 types: this.dumpConfig[req].types, 669 }; 670 }); 671 Analytics.Tracing.logCollectDumps(requestedTraceTypes.map((t) => t.name)); 672 673 const requestedDumpsWithConfig: UserRequest[] = requestedDumps.map( 674 (target) => { 675 const enabledConfig = this.requestedEnabledConfig( 676 target, 677 this.dumpConfig, 678 ); 679 const selectedConfig = this.requestedSelectedConfig( 680 target, 681 this.dumpConfig, 682 ); 683 return { 684 target, 685 config: enabledConfig.concat(selectedConfig), 686 }; 687 }, 688 ); 689 690 const controller = assertDefined(this.controller); 691 const device = assertDefined(this.selectedDevice); 692 await this.setState(ConnectionState.DUMPING_STATE); 693 await controller.dumpState(device, requestedDumpsWithConfig); 694 this.refreshDumps = false; 695 if (this.state === ConnectionState.DUMPING_STATE) { 696 this.filesCollected.emit({ 697 requested: requestedTraceTypes, 698 collected: await this.fetchLastSessionData(), 699 }); 700 } 701 } 702 703 async endTrace() { 704 if (!this.selectedDevice) { 705 return; 706 } 707 const controller = assertDefined(this.controller); 708 await this.setState(ConnectionState.ENDING_TRACE); 709 await controller.endTrace(this.selectedDevice); 710 if (this.state === ConnectionState.ENDING_TRACE) { 711 this.filesCollected.emit({ 712 requested: this.requestedTraceTypes, 713 collected: await this.fetchLastSessionData(), 714 }); 715 } 716 } 717 718 getDeviceName(device: AdbDeviceConnection): string { 719 return device.getFormattedName(); 720 } 721 722 showTryAuthorizeButton(device: AdbDeviceConnection): boolean { 723 return ( 724 device.getState() === AdbDeviceState.UNAUTHORIZED && 725 this.getConnectionType() === AdbConnectionType.WDP 726 ); 727 } 728 729 getSelectedDevice(): string { 730 return this.getDeviceName(assertDefined(this.selectedDevice)); 731 } 732 733 getDeviceStateIcon(state: AdbDeviceState): string { 734 switch (state) { 735 case AdbDeviceState.AVAILABLE: 736 return 'smartphone'; 737 case AdbDeviceState.UNAUTHORIZED: 738 return 'screen_lock_portrait'; 739 case AdbDeviceState.OFFLINE: 740 return 'mobile_off'; 741 default: 742 assertUnreachable(state); 743 } 744 } 745 746 isTracing(): boolean { 747 return this.tracingSessionStates.includes(this.state); 748 } 749 750 isTracingOrLoading(): boolean { 751 return this.isTracing() || this.isLoadOperationInProgress(); 752 } 753 754 isDumpingState(): boolean { 755 return ( 756 this.refreshDumps || 757 this.state === ConnectionState.DUMPING_STATE || 758 this.isLoadOperationInProgress() 759 ); 760 } 761 762 disableTraceSection(): boolean { 763 return this.isTracingOrLoading() || this.refreshDumps; 764 } 765 766 async fetchExistingTraces() { 767 const controller = assertDefined(this.controller); 768 const files = await this.fetchLastSessionData(); 769 this.filesCollected.emit({ 770 requested: [], 771 collected: files, 772 }); 773 if (files.length === 0) { 774 await controller.restartConnection(); 775 } 776 } 777 778 onAvailableTracesChange( 779 newTraces: UiTraceTarget[], 780 removedTraces: UiTraceTarget[], 781 ) { 782 newTraces.forEach((trace) => { 783 const config = assertDefined(this.traceConfig)[trace]; 784 config.available = true; 785 }); 786 removedTraces.forEach((trace) => { 787 const config = assertDefined(this.traceConfig)[trace]; 788 config.available = false; 789 }); 790 } 791 792 onDevicesChange(devices: AdbDeviceConnection[]) { 793 if (!this.selectedDevice) { 794 return; 795 } 796 const device = devices.find( 797 (d) => d.id === assertDefined(this.selectedDevice).id, 798 ); 799 if (!device) { 800 return; 801 } 802 const screenRecordingConfig = assertDefined(this.traceConfig)[ 803 UiTraceTarget.SCREEN_RECORDING 804 ].config; 805 const displaysConfig = assertDefined( 806 screenRecordingConfig.selectionConfigs.find((c) => c.key === 'displays'), 807 ); 808 const multiDisplay = device.hasMultiDisplayScreenRecording(); 809 const displays = device.getDisplays(); 810 811 if (multiDisplay && !Array.isArray(displaysConfig.value)) { 812 screenRecordingConfig.selectionConfigs = 813 makeScreenRecordingSelectionConfigs(displays, []); 814 } else if (!multiDisplay && Array.isArray(displaysConfig.value)) { 815 screenRecordingConfig.selectionConfigs = 816 makeScreenRecordingSelectionConfigs(displays, ''); 817 } else { 818 screenRecordingConfig.selectionConfigs[0].options = displays; 819 } 820 821 const screenshotConfig = assertDefined(this.dumpConfig)[ 822 UiTraceTarget.SCREENSHOT 823 ].config; 824 assertDefined( 825 screenshotConfig.selectionConfigs.find((c) => c.key === 'displays'), 826 ).options = displays; 827 this.changeDetectorRef.detectChanges(); 828 } 829 830 async onError(errorText: string) { 831 await this.setState(ConnectionState.ERROR, errorText); 832 } 833 834 async onConnectionStateChange(newState: ConnectionState): Promise<void> { 835 switch (newState) { 836 case ConnectionState.IDLE: 837 if (this.state === ConnectionState.CONNECTING) { 838 await this.setState(newState); 839 } 840 return; 841 case ConnectionState.CONNECTING: 842 await this.setState(newState); 843 return; 844 default: 845 if (newState !== this.state) { 846 await this.setState(newState); 847 } 848 } 849 } 850 851 private async changeHostConnection(adbConnectionType: string) { 852 if (this.selectedDevice) { 853 await this.controller?.onDestroy(this.selectedDevice); 854 } 855 this.controller = new TraceCollectionController(adbConnectionType, this); 856 this.storage?.add(this.storeKeyAdbConnectionType, adbConnectionType); 857 await this.controller.restartConnection(); 858 } 859 860 private async requestTraces(requestedTraces: UiTraceTarget[]) { 861 this.requestedTraceTypes = requestedTraces.map((req) => { 862 return { 863 name: this.traceConfig[req].name, 864 types: this.traceConfig[req].types, 865 }; 866 }); 867 Analytics.Tracing.logCollectTraces( 868 this.requestedTraceTypes.map((t) => t.name), 869 ); 870 871 if (requestedTraces.length === 0) { 872 this.emitEvent(new NoTraceTargetsSelected()); 873 return; 874 } 875 876 const requestedTracesWithConfig: UserRequest[] = requestedTraces.map( 877 (target) => { 878 const enabledConfig = this.requestedEnabledConfig( 879 target, 880 this.traceConfig, 881 ); 882 const selectedConfig = this.requestedSelectedConfig( 883 target, 884 this.traceConfig, 885 ); 886 return { 887 target, 888 config: enabledConfig.concat(selectedConfig), 889 }; 890 }, 891 ); 892 const startTimeMs = Date.now(); 893 await this.setState(ConnectionState.STARTING_TRACE); 894 await assertDefined(this.controller).startTrace( 895 assertDefined(this.selectedDevice), 896 requestedTracesWithConfig, 897 ); 898 if (this.state === ConnectionState.STARTING_TRACE) { 899 Analytics.Tracing.logStartTime(Date.now() - startTimeMs); 900 await this.setState(ConnectionState.TRACING); 901 } 902 } 903 904 private async fetchLastSessionData() { 905 await this.setState(ConnectionState.LOADING_DATA); 906 const startTimeMs = Date.now(); 907 const files = await assertDefined(this.controller).fetchLastSessionData( 908 assertDefined(this.selectedDevice), 909 ); 910 if (files.length === 0) { 911 Analytics.Proxy.logNoFilesFound(); 912 } 913 const size = files.reduce((total, file) => (total += file.size), 0); 914 Analytics.Loading.logFileExtractionTime( 915 'device', 916 Date.now() - startTimeMs, 917 size, 918 ); 919 return files; 920 } 921 922 private getRequests(configMap: TraceConfigurationMap): UiTraceTarget[] { 923 return Object.keys(configMap) 924 .filter((dumpKey: string) => { 925 return configMap[dumpKey].config.enabled && dumpKey in UiTraceTarget; 926 }) 927 .map((key) => Number(key)) as UiTraceTarget[]; 928 } 929 930 private requestedEnabledConfig( 931 target: UiTraceTarget, 932 configMap: TraceConfigurationMap, 933 ): UserRequestConfig[] { 934 const req: UserRequestConfig[] = []; 935 const trace = configMap[target]; 936 assertTrue(trace?.config.enabled ?? false); 937 trace.config.checkboxConfigs.forEach((con: CheckboxConfiguration) => { 938 if (con.enabled) { 939 req.push({key: con.key}); 940 } 941 }); 942 return req; 943 } 944 945 private requestedSelectedConfig( 946 target: UiTraceTarget, 947 configMap: TraceConfigurationMap, 948 ): UserRequestConfig[] { 949 const trace = configMap[target]; 950 assertTrue(trace?.config.enabled ?? false); 951 return trace.config.selectionConfigs.map((con: SelectionConfiguration) => { 952 return {key: con.key, value: con.value}; 953 }); 954 } 955 956 private async setState(newState: ConnectionState, errorText = '') { 957 this.updateProgressMessage(newState); 958 959 const controller = assertDefined(this.controller); 960 961 this.state = newState; 962 this.errorText = errorText; 963 this.changeDetectorRef.detectChanges(); 964 965 const maybeRefreshDumps = 966 this.refreshDumps && 967 newState !== ConnectionState.LOADING_DATA && 968 newState !== ConnectionState.CONNECTING; 969 if ( 970 maybeRefreshDumps && 971 newState === ConnectionState.IDLE && 972 this.selectedDevice 973 ) { 974 await this.dumpState(); 975 } else if (maybeRefreshDumps) { 976 // device is not connected or proxy is not started/invalid/in error state 977 // so cannot refresh dump automatically 978 this.refreshDumps = false; 979 } 980 981 const deviceRequestStates = [ 982 ConnectionState.IDLE, 983 ConnectionState.CONNECTING, 984 ]; 985 if (!deviceRequestStates.includes(newState)) { 986 controller.cancelDeviceRequests(); 987 } 988 989 switch (newState) { 990 case ConnectionState.TRACE_TIMEOUT: 991 UserNotifier.add(new ProxyTraceTimeout()); 992 await this.endTrace(); 993 return; 994 case ConnectionState.NOT_FOUND: 995 Analytics.Proxy.logServerNotFound(controller.getConnectionType()); 996 return; 997 998 case ConnectionState.ERROR: 999 Analytics.Error.logProxyError(this.errorText); 1000 return; 1001 1002 case ConnectionState.CONNECTING: 1003 await controller.requestDevices(); 1004 return; 1005 1006 case ConnectionState.IDLE: { 1007 await this.selectedDevice?.updateAvailableTraces(); 1008 return; 1009 } 1010 default: 1011 // do nothing 1012 } 1013 } 1014 1015 private updateProgressMessage(newState: ConnectionState) { 1016 switch (newState) { 1017 case ConnectionState.STARTING_TRACE: 1018 this.progressMessage = 'Starting trace...'; 1019 this.progressIcon = 'cable'; 1020 this.progressPercentage = undefined; 1021 break; 1022 case ConnectionState.TRACING: 1023 this.progressMessage = 'Tracing...'; 1024 this.progressIcon = 'cable'; 1025 this.progressPercentage = undefined; 1026 break; 1027 case ConnectionState.ENDING_TRACE: 1028 this.progressMessage = 'Ending trace...'; 1029 this.progressIcon = 'cable'; 1030 break; 1031 case ConnectionState.DUMPING_STATE: 1032 this.progressMessage = 'Dumping state...'; 1033 this.progressIcon = 'cable'; 1034 break; 1035 default: 1036 this.progressIcon = 'sync'; 1037 } 1038 } 1039} 1040