| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| import {CommonModule} from '@angular/common'; |
| import { |
| ChangeDetectorRef, |
| Component, |
| ErrorHandler, |
| Inject, |
| Injector, |
| NgZone, |
| ViewChild, |
| ViewEncapsulation, |
| } from '@angular/core'; |
| import {createCustomElement} from '@angular/elements'; |
| import {FormControl, ReactiveFormsModule, Validators} from '@angular/forms'; |
| import {MatButtonModule} from '@angular/material/button'; |
| import {MatDialog, MatDialogModule} from '@angular/material/dialog'; |
| import {MatDividerModule} from '@angular/material/divider'; |
| import {MatFormFieldModule} from '@angular/material/form-field'; |
| import {MatIconModule} from '@angular/material/icon'; |
| import {MatInputModule} from '@angular/material/input'; |
| import {MatProgressBarModule} from '@angular/material/progress-bar'; |
| import {MatToolbarModule} from '@angular/material/toolbar'; |
| import {MatTooltipModule} from '@angular/material/tooltip'; |
| import {Title} from '@angular/platform-browser'; |
| import {AbtChromeExtensionProtocol} from 'abt_chrome_extension/abt_chrome_extension_protocol'; |
| import {GlobalErrorHandler} from 'app/global_error_handler'; |
| import {Mediator} from 'app/mediator'; |
| import {TimelineData} from 'app/timeline_data'; |
| import {TracePipeline} from 'app/trace_pipeline'; |
| import {DownloadRequest, downloadFromUrl} from 'common/download'; |
| import {DOWNLOAD_FILENAME_REGEX} from 'common/io'; |
| import {globalConfig} from 'common/global_config'; |
| import {InMemoryStorage} from 'common/store/in_memory_storage'; |
| import {PersistentStore} from 'common/store/persistent_store'; |
| import {Store} from 'common/store/store'; |
| import {Timestamp} from 'common/time/time'; |
| import {getRootUrl} from 'common/window'; |
| import {CrossToolProtocol} from 'cross_tool/cross_tool_protocol'; |
| import {Analytics} from 'logging/analytics'; |
| import {ProgressListener} from 'messaging/progress_listener'; |
| import { |
| AppFilesCollected, |
| AppFilesUploaded, |
| AppInitialized, |
| AppRefreshDumpsRequest, |
| AppResetRequest, |
| AppTraceViewRequest, |
| BugreportFileSelected, |
| DarkModeToggled, |
| WinscopeEvent, |
| WinscopeEventType, |
| } from 'messaging/winscope_event'; |
| import {WinscopeEventListener} from 'messaging/winscope_event_listener'; |
| import {UserNotifier} from 'services/user_notifier'; |
| import {AdbFiles} from 'trace_collection/adb_files'; |
| import {iconDividerStyle} from 'viewers/components/styles/icon_divider.styles'; |
| import {ViewerInputMethodComponent} from 'viewers/components/viewer_input_method_component'; |
| import {Viewer} from 'viewers/viewer'; |
| import {ViewerInputComponent} from 'viewers/viewer_input/viewer_input_component'; |
| import {ViewerJankCujsComponent} from 'viewers/viewer_jank_cujs/viewer_jank_cujs_component'; |
| import {ViewerMediaBasedComponent} from 'viewers/viewer_media_based/viewer_media_based_component'; |
| import {ViewerProtologComponent} from 'viewers/viewer_protolog/viewer_protolog_component'; |
| import {ViewerSearchComponent} from 'viewers/viewer_search/viewer_search_component'; |
| import {ViewerSurfaceFlingerComponent} from 'viewers/viewer_surface_flinger/viewer_surface_flinger_component'; |
| import {ViewerTransactionsComponent} from 'viewers/viewer_transactions/viewer_transactions_component'; |
| import {ViewerTransitionsComponent} from 'viewers/viewer_transitions/viewer_transitions_component'; |
| import {ViewerViewCaptureComponent} from 'viewers/viewer_view_capture/viewer_view_capture_component'; |
| import {ViewerWindowManagerComponent} from 'viewers/viewer_window_manager/viewer_window_manager_component'; |
| import {OriginAllowList} from 'cross_tool/origin_allow_list'; |
| import { |
| MatDrawer, |
| MatDrawerContainer, |
| MatDrawerContent, |
| } from './bottomnav/bottom_drawer_component'; |
| import {CollectTracesComponent} from './collect_traces_component'; |
| import {ShortcutsComponent} from './shortcuts_component'; |
| import {SnackBarOpener} from './snack_bar_opener'; |
| import {TimelineComponent} from './timeline/timeline_component'; |
| import {TraceViewComponent} from './trace_view_component'; |
| import {UploadTracesComponent} from './upload_traces_component'; |
| import { |
| WarningDialogComponent, |
| WarningDialogData, |
| WarningDialogResult, |
| } from './warning_dialog_component'; |
| |
| /** |
| * The root component of the Winscope app. |
| */ |
| @Component({ |
| selector: 'app-root', |
| encapsulation: ViewEncapsulation.None, |
| standalone: true, |
| imports: [ |
| CommonModule, |
| MatToolbarModule, |
| MatButtonModule, |
| MatTooltipModule, |
| MatIconModule, |
| MatFormFieldModule, |
| MatInputModule, |
| ReactiveFormsModule, |
| MatProgressBarModule, |
| MatDividerModule, |
| MatDialogModule, |
| MatDrawer, |
| MatDrawerContainer, |
| MatDrawerContent, |
| TraceViewComponent, |
| TimelineComponent, |
| CollectTracesComponent, |
| UploadTracesComponent, |
| ShortcutsComponent, |
| WarningDialogComponent, |
| ], |
| providers: [Title, {provide: ErrorHandler, useClass: GlobalErrorHandler}], |
| template: ` |
| <mat-toolbar class="toolbar"> |
| <div class="horizontal-align vertical-align fixed"> |
| <img class="app-title" [src]="getLogoUrl()"/> |
| @if (isBeta) { |
| <span class="beta-tag">BETA</span> |
| } |
| </div> |
| |
| <div class="horizontal-align vertical-align icon-actions"> |
| @if (showDataLoadedElements) { |
| <div class="download-files-section"> |
| <div |
| class="file-descriptor vertical-align" |
| [class.file-warning]="packetLossWarning() !== undefined"> |
| @if (showCrossToolSyncButton()) { |
| <button |
| mat-icon-button |
| [matTooltip]="getCrossToolSyncTooltip()" |
| class="cross-tool-sync-button no-touch-target-button" |
| (click)="onCrossToolSyncButtonClick()" |
| [color]="getCrossToolSyncButtonColor()"> |
| <mat-icon class="material-symbols-outlined">cloud_sync</mat-icon> |
| </button> |
| } |
| @if (packetLossWarning()) { |
| <mat-icon |
| [matTooltip]="packetLossWarning()" |
| class="warning-icon fixed">warning</mat-icon> |
| } |
| @if (!isEditingFilename) { |
| <span class="download-file-info text-no-overflow mat-body-2"> |
| {{ filenameFormControl.value }} |
| </span> |
| } |
| @if (!isEditingFilename) { |
| <span class="download-file-ext mat-body-2">.zip</span> |
| } |
| @if (isEditingFilename) { |
| <mat-form-field |
| class="file-name-input-field" |
| subscriptSizing="dynamic" |
| floatLabel="always" |
| (keydown.esc)="trySubmitFilename()" |
| (keydown.enter)="trySubmitFilename()" |
| (focusout)="trySubmitFilename()" |
| matTooltip="Allowed: A-Z a-z 0-9 . _ - #"> |
| <mat-label>Edit file name</mat-label> |
| <input matInput class="right-align" [formControl]="filenameFormControl" /> |
| <span matTextSuffix>.zip</span> |
| </mat-form-field> |
| } |
| @if (isEditingFilename) { |
| <button |
| mat-icon-button |
| class="check-button no-touch-target-button" |
| matTooltip="Submit file name" |
| (click)="trySubmitFilename()"> |
| <mat-icon>check</mat-icon> |
| </button> |
| } |
| @if (!isEditingFilename) { |
| <button |
| mat-icon-button |
| class="edit-button no-touch-target-button" |
| matTooltip="Edit file name" |
| (click)="onPencilIconClick()"> |
| <mat-icon>edit</mat-icon> |
| </button> |
| } |
| <button |
| mat-icon-button |
| [disabled]="isEditingFilename" |
| matTooltip="Download all traces" |
| class="save-button no-touch-target-button" |
| (click)="onDownloadTracesButtonClick()"> |
| <mat-icon class="material-symbols-outlined">download</mat-icon> |
| </button> |
| </div> |
| @if (downloadProgress !== undefined) { |
| <mat-progress-bar |
| mode="determinate" |
| [value]="downloadProgress"> |
| </mat-progress-bar> |
| } |
| </div> |
| } |
| |
| @if (showDataLoadedElements) { |
| <div class="icon-divider toolbar-icon-divider"></div> |
| } |
| @if (showDataLoadedElements && allTracesAreDumps()) { |
| <button |
| color="primary" |
| mat-icon-button |
| matTooltip="Refresh dumps" |
| class="refresh-dumps" |
| (click)="onRefreshDumpsButtonClick()"> |
| <mat-icon class="material-symbols-outlined">refresh</mat-icon> |
| </button> |
| } |
| @if (showDataLoadedElements) { |
| <button |
| mat-icon-button |
| matTooltip="Upload or collect new trace" |
| class="upload-new" |
| (click)="onUploadNewButtonClick()"> |
| <mat-icon class="material-symbols-outlined">upload</mat-icon> |
| </button> |
| } |
| |
| <button |
| mat-icon-button |
| matTooltip="Shortcuts" |
| class="shortcuts" |
| (click)="openShortcutsPanel()"> |
| <mat-icon>keyboard_command_key</mat-icon> |
| </button> |
| |
| <button |
| mat-icon-button |
| matTooltip="Documentation" |
| class="documentation" |
| (click)="goToDocumentation()"> |
| <mat-icon>menu_book</mat-icon> |
| </button> |
| |
| <button |
| mat-icon-button |
| class="report-bug" |
| matTooltip="Report bug" |
| (click)="goToBuganizer()"> |
| <mat-icon>bug_report</mat-icon> |
| </button> |
| |
| <button |
| mat-icon-button |
| class="dark-mode" |
| matTooltip="Switch to {{ isDarkModeOn ? 'light' : 'dark' }} mode" |
| (click)="toggleDarkMode()"> |
| <mat-icon> |
| {{ isDarkModeOn ? 'brightness_5' : 'brightness_4' }} |
| </mat-icon> |
| </button> |
| |
| @if (isInsideWinscopeProxyFrame()) { |
| <button |
| mat-icon-button |
| class="iframe-settings" |
| matTootltip="Settings" |
| (click)="openSettings()"> |
| <mat-icon>settings</mat-icon> |
| </button> |
| } |
| </div> |
| </mat-toolbar> |
| |
| <mat-divider></mat-divider> |
| |
| <mat-drawer-container autosize disableClose autoFocus> |
| <mat-drawer-content> |
| @if (dataLoaded) { |
| <trace-view class="viewers" [viewers]="viewers" [store]="persistentStore"></trace-view> |
| |
| <mat-divider></mat-divider> |
| } @else { |
| <div class="center"> |
| <div class="landing-content"> |
| <h1 class="welcome-info mat-headline-1"> |
| Welcome to Winscope. Please select source to view traces. |
| </h1> |
| |
| <div class="card-grid landing-grid"> |
| <collect-traces |
| class="collect-traces-card homepage-card" |
| [storage]="appStorage" |
| (filesCollected)="onFilesCollected($event)"></collect-traces> |
| |
| <upload-traces |
| #uploadTraces |
| class="upload-traces-card homepage-card" |
| [tracePipeline]="tracePipeline" |
| [storage]="appStorage" |
| (filesUploaded)="onFilesUploaded($event)" |
| (viewTracesButtonClick)="onViewTracesButtonClick($event)" |
| (downloadTracesClick)="onDownloadTracesButtonClick(uploadTraces)"></upload-traces> |
| </div> |
| </div> |
| </div> |
| } |
| </mat-drawer-content> |
| |
| <mat-drawer #drawer mode="overlay" opened="true" [baseHeight]="collapsedTimelineHeight"> |
| @if (dataLoaded) { |
| <timeline |
| [allTraces]="tracePipeline.getTraces()" |
| [timelineData]="timelineData" |
| [store]="persistentStore" |
| [initialTabTraceType]="mediator.initialTimelineTabTraceType" |
| (collapsedTimelineSizeChanged)="onCollapsedTimelineSizeChanged($event)"></timeline> |
| } |
| </mat-drawer> |
| </mat-drawer-container> |
| `, |
| styles: [ |
| ` |
| .toolbar { |
| gap: 10px; |
| justify-content: space-between; |
| min-height: 64px; |
| } |
| .app-title { |
| height: 100%; |
| } |
| .beta-tag { |
| vertical-align: super; |
| text-size-adjust: 10%; |
| font-size: 0.8rem; |
| margin-top: -0.8rem; |
| margin-left: 0.2rem; |
| color: var(--logo-blue); |
| font-weight: 800; |
| } |
| .welcome-info { |
| margin: 16px 0 6px 0; |
| text-align: center; |
| } |
| .homepage-card { |
| display: flex; |
| flex-direction: column; |
| flex: 1; |
| overflow: auto; |
| height: 870px; |
| } |
| .horizontal-align { |
| justify-content: center; |
| } |
| .vertical-align { |
| text-align: center; |
| align-items: center; |
| overflow-x: hidden; |
| display: flex; |
| } |
| .fixed { |
| min-width: fit-content; |
| } |
| .icon-actions { |
| height: 100%; |
| } |
| .download-files-section { |
| overflow-x: hidden; |
| } |
| .file-descriptor { |
| font-size: 14px; |
| padding-left: 10px; |
| max-width: 750px; |
| height: 100%; |
| } |
| .file-warning { |
| border: solid 2px var(--warning-color); |
| background: var(--warning-background-color); |
| } |
| .file-descriptor .warning-icon { |
| padding-inline-end: 4px; |
| } |
| .download-file-info { |
| padding-top: 3px; |
| max-width: 650px; |
| } |
| .download-file-ext { |
| padding-top: 3px; |
| } |
| .file-name-input-field .right-align { |
| text-align: right; |
| } |
| .file-name-input-field .mat-mdc-text-field-wrapper { |
| width: 600px; |
| max-width: 100%; |
| } |
| .toolbar-icon-divider { |
| margin-right: 6px; |
| margin-left: 6px; |
| height: 20px; |
| } |
| .viewers { |
| height: 0; |
| flex-grow: 1; |
| display: flex; |
| flex-direction: column; |
| overflow: auto; |
| } |
| .center { |
| display: flex; |
| align-content: center; |
| flex-direction: column; |
| justify-content: center; |
| align-items: center; |
| justify-items: center; |
| flex-grow: 1; |
| background-color: var(--background-color); |
| } |
| .landing-content { |
| width: 100%; |
| } |
| .landing-content .card-grid { |
| max-width: 1800px; |
| flex-grow: 1; |
| margin: auto; |
| } |
| `, |
| iconDividerStyle, |
| ], |
| }) |
| export class AppComponent implements WinscopeEventListener { |
| title = 'winscope'; |
| timelineData = new TimelineData(); |
| abtChromeExtensionProtocol = new AbtChromeExtensionProtocol(); |
| crossToolProtocol: CrossToolProtocol; |
| dataLoaded = false; |
| showDataLoadedElements = false; |
| isBeta = /beta(_[a-z]+)?\/index\.html/.test(window.location.href); |
| collapsedTimelineHeight = 0; |
| isEditingFilename = false; |
| persistentStore = new PersistentStore(); |
| viewers: Viewer[] = []; |
| |
| isDarkModeOn = false; |
| changeDetectorRef: ChangeDetectorRef; |
| tracePipeline: TracePipeline; |
| mediator: Mediator; |
| currentTimestamp?: Timestamp; |
| filenameFormControl = new FormControl( |
| 'winscope', |
| Validators.compose([ |
| Validators.required, |
| Validators.pattern(DOWNLOAD_FILENAME_REGEX), |
| ]), |
| ); |
| |
| appStorage: Store; |
| downloadProgress: number | undefined; |
| downloadRequest: DownloadRequest = (url: string, fileName: string) => { |
| downloadFromUrl(url, fileName); |
| }; |
| |
| @ViewChild(UploadTracesComponent) |
| uploadTracesComponent?: UploadTracesComponent; |
| @ViewChild(CollectTracesComponent) |
| collectTracesComponent?: CollectTracesComponent; |
| @ViewChild(TraceViewComponent) traceViewComponent?: TraceViewComponent; |
| @ViewChild(TimelineComponent) timelineComponent?: TimelineComponent; |
| |
| constructor( |
| @Inject(Injector) injector: Injector, |
| @Inject(ChangeDetectorRef) changeDetectorRef: ChangeDetectorRef, |
| @Inject(SnackBarOpener) snackbarOpener: SnackBarOpener, |
| @Inject(Title) private pageTitle: Title, |
| @Inject(NgZone) private ngZone: NgZone, |
| @Inject(MatDialog) private dialog: MatDialog, |
| ) { |
| this.changeDetectorRef = changeDetectorRef; |
| UserNotifier.setSnackBarOpener(snackbarOpener); |
| this.tracePipeline = new TracePipeline(); |
| this.crossToolProtocol = new CrossToolProtocol( |
| this.tracePipeline.getTimestampConverter(), |
| ); |
| this.mediator = new Mediator( |
| this.tracePipeline, |
| this.timelineData, |
| this.abtChromeExtensionProtocol, |
| this.crossToolProtocol, |
| this, |
| new PersistentStore(), |
| ); |
| |
| const storeDarkMode = this.persistentStore.get('dark-mode'); |
| const prefersDarkQuery = window.matchMedia?.( |
| '(prefers-color-scheme: dark)', |
| ); |
| this.setDarkMode( |
| storeDarkMode ? storeDarkMode === 'true' : prefersDarkQuery.matches, |
| ); |
| |
| if (!customElements.get('viewer-input-method')) { |
| customElements.define( |
| 'viewer-input-method', |
| createCustomElement(ViewerInputMethodComponent, {injector}), |
| ); |
| } |
| if (!customElements.get('viewer-protolog')) { |
| customElements.define( |
| 'viewer-protolog', |
| createCustomElement(ViewerProtologComponent, {injector}), |
| ); |
| } |
| if (!customElements.get('viewer-media-based')) { |
| customElements.define( |
| 'viewer-media-based', |
| createCustomElement(ViewerMediaBasedComponent, {injector}), |
| ); |
| } |
| if (!customElements.get('viewer-surface-flinger')) { |
| customElements.define( |
| 'viewer-surface-flinger', |
| createCustomElement(ViewerSurfaceFlingerComponent, {injector}), |
| ); |
| } |
| if (!customElements.get('viewer-transactions')) { |
| customElements.define( |
| 'viewer-transactions', |
| createCustomElement(ViewerTransactionsComponent, {injector}), |
| ); |
| } |
| if (!customElements.get('viewer-window-manager')) { |
| customElements.define( |
| 'viewer-window-manager', |
| createCustomElement(ViewerWindowManagerComponent, {injector}), |
| ); |
| } |
| if (!customElements.get('viewer-transitions')) { |
| customElements.define( |
| 'viewer-transitions', |
| createCustomElement(ViewerTransitionsComponent, {injector}), |
| ); |
| } |
| if (!customElements.get('viewer-view-capture')) { |
| customElements.define( |
| 'viewer-view-capture', |
| createCustomElement(ViewerViewCaptureComponent, {injector}), |
| ); |
| } |
| if (!customElements.get('viewer-jank-cujs')) { |
| customElements.define( |
| 'viewer-jank-cujs', |
| createCustomElement(ViewerJankCujsComponent, {injector}), |
| ); |
| } |
| if (!customElements.get('viewer-input')) { |
| customElements.define( |
| 'viewer-input', |
| createCustomElement(ViewerInputComponent, {injector}), |
| ); |
| } |
| if (!customElements.get('viewer-search')) { |
| customElements.define( |
| 'viewer-search', |
| createCustomElement(ViewerSearchComponent, {injector}), |
| ); |
| } |
| |
| this.appStorage = |
| globalConfig.MODE === 'PROD' |
| ? new PersistentStore() |
| : new InMemoryStorage(); |
| |
| window.onunhandledrejection = (evt) => { |
| Analytics.Error.logGlobalException(evt.reason); |
| }; |
| } |
| |
| async ngAfterViewInit() { |
| await this.mediator.onWinscopeEvent(new AppInitialized()); |
| } |
| |
| ngAfterViewChecked() { |
| this.mediator.setUploadTracesComponent(this.uploadTracesComponent); |
| this.mediator.setCollectTracesComponent(this.collectTracesComponent); |
| this.mediator.setTraceViewComponent(this.traceViewComponent); |
| this.mediator.setTimelineComponent(this.timelineComponent); |
| } |
| |
| onCollapsedTimelineSizeChanged(height: number) { |
| this.collapsedTimelineHeight = height; |
| this.changeDetectorRef.detectChanges(); |
| } |
| |
| getLogoUrl(): string { |
| const logoPath = this.isDarkModeOn |
| ? 'logo_dark_mode.svg' |
| : 'logo_light_mode.svg'; |
| return getRootUrl() + logoPath; |
| } |
| |
| async setDarkMode(enabled: boolean) { |
| document.body.classList.toggle('dark-mode', enabled); |
| this.persistentStore.add('dark-mode', `${enabled}`); |
| this.isDarkModeOn = enabled; |
| await this.mediator.onWinscopeEvent(new DarkModeToggled(enabled)); |
| } |
| |
| onPencilIconClick() { |
| this.isEditingFilename = true; |
| } |
| |
| trySubmitFilename() { |
| if (this.filenameFormControl.invalid) { |
| return; |
| } |
| this.isEditingFilename = false; |
| this.pageTitle.setTitle(`Winscope | ${this.filenameFormControl.value}`); |
| } |
| |
| async onDownloadTracesButtonClick(progressListener: ProgressListener = this) { |
| if (this.filenameFormControl.invalid) { |
| return; |
| } |
| const archiveBlob = |
| await this.tracePipeline.makeZipArchiveWithLoadedTraceFiles( |
| (perc: number) => { |
| progressListener.onProgressUpdate('Downloading', 90 * perc); |
| }, |
| ); |
| const archiveFilename = `${ |
| this.showDataLoadedElements |
| ? this.filenameFormControl.value |
| : this.tracePipeline.getDownloadArchiveFilename() |
| }.zip`; |
| this.downloadTraces(archiveBlob, archiveFilename); |
| progressListener.onOperationFinished(true); |
| } |
| |
| async onFilesCollected(files: AdbFiles) { |
| await this.mediator.onWinscopeEvent(new AppFilesCollected(files)); |
| } |
| |
| async onFilesUploaded(files: File[]) { |
| await this.mediator.onWinscopeEvent(new AppFilesUploaded(files)); |
| } |
| |
| async onRefreshDumpsButtonClick() { |
| Analytics.Tracing.logRefreshDumps(); |
| await this.mediator.onWinscopeEvent(new AppRefreshDumpsRequest()); |
| } |
| |
| async onUploadNewButtonClick() { |
| await this.mediator.onWinscopeEvent(new AppResetRequest()); |
| this.persistentStore.clear('treeView'); |
| } |
| |
| async onViewTracesButtonClick(discardLegacyTraces: boolean) { |
| await this.mediator.onWinscopeEvent( |
| new AppTraceViewRequest(discardLegacyTraces), |
| ); |
| } |
| |
| onProgressUpdate(message: string, progressPercentage: number | undefined) { |
| this.ngZone.run(() => { |
| this.downloadProgress = progressPercentage; |
| }); |
| } |
| |
| onOperationFinished(success: boolean) { |
| this.ngZone.run(() => { |
| this.downloadProgress = undefined; |
| }); |
| } |
| |
| async onWinscopeEvent(event: WinscopeEvent) { |
| await event.visit(WinscopeEventType.VIEWERS_LOADED, async (event) => { |
| this.viewers = event.viewers; |
| this.filenameFormControl.setValue( |
| this.tracePipeline.getDownloadArchiveFilename(), |
| ); |
| this.pageTitle.setTitle(`Winscope | ${this.filenameFormControl.value}`); |
| this.isEditingFilename = false; |
| |
| // some elements e.g. timeline require dataLoaded to be set outside NgZone to render |
| this.dataLoaded = true; |
| this.changeDetectorRef.detectChanges(); |
| |
| // tooltips must be rendered inside ngZone due to limitation of MatTooltip, |
| // therefore toolbar elements controlled by a different boolean |
| this.ngZone.run(() => { |
| this.showDataLoadedElements = true; |
| }); |
| }); |
| |
| await event.visit(WinscopeEventType.VIEWERS_UNLOADED, async (event) => { |
| this.dataLoaded = false; |
| this.showDataLoadedElements = false; |
| this.pageTitle.setTitle('Winscope'); |
| this.changeDetectorRef.detectChanges(); |
| }); |
| |
| await event.visit( |
| WinscopeEventType.BUGREPORT_FILE_SELECTION_REQUEST, |
| async (event) => { |
| await this.showFileSelectionDialog(event.filenames); |
| }, |
| ); |
| } |
| |
| openShortcutsPanel() { |
| this.dialog.open(ShortcutsComponent, { |
| height: 'fit-content', |
| maxWidth: '860px', |
| panelClass: 'shortcuts-panel', |
| }); |
| } |
| |
| goToDocumentation() { |
| Analytics.Help.logDocumentationOpened(); |
| this.goToLink( |
| 'https://source.android.com/docs/core/graphics/tracing-win-transitions', |
| ); |
| } |
| |
| goToBuganizer() { |
| Analytics.Help.logBuganizerOpened(); |
| this.goToLink('https://b.corp.google.com/issues/new?component=909476'); |
| } |
| |
| toggleDarkMode() { |
| if (!this.isDarkModeOn) { |
| Analytics.Settings.logDarkModeEnabled(); |
| } |
| this.setDarkMode(!this.isDarkModeOn); |
| } |
| |
| isInsideWinscopeProxyFrame(): boolean { |
| // NOTE: Technically anyone can pass whatever they want as the origin parameter, |
| // but that is fine; in those cases we would just show a settings button that does nothing, |
| // because we would fail posting the message due to origin check failures. |
| const reportedParentOrigin = this.getReportedParentOrigin(); |
| if ( |
| !reportedParentOrigin || |
| !this.isSupportedReportedParentOrigin(reportedParentOrigin) |
| ) { |
| return false; |
| } |
| |
| try { |
| return window.self !== window.top; |
| } catch (e) { |
| // Catch potential cross-origin errors when accessing window.top |
| return true; |
| } |
| } |
| |
| getReportedParentOrigin() { |
| const urlParams = new URLSearchParams(window.location.search); |
| return urlParams.get('parentOrigin'); |
| } |
| |
| isSupportedReportedParentOrigin(parentOrigin: string): boolean { |
| return OriginAllowList.isAllowedIframeParentOrigin(parentOrigin); |
| } |
| |
| openSettings() { |
| const parentOrigin = this.getReportedParentOrigin(); |
| |
| if (parentOrigin == null) { |
| console.warn( |
| "Provided 'parentOrigin' is null cannot send request to open settings menu", |
| ); |
| return; |
| } |
| |
| // Check if inside an iframe |
| if ( |
| this.isInsideWinscopeProxyFrame() && |
| this.isSupportedReportedParentOrigin(parentOrigin) |
| ) { |
| // Send message to the parent window |
| console.log('Sending message to parent window...', window.parent.origin); |
| window.parent.postMessage({winscopeAction: 'openSettings'}, parentOrigin); |
| } else { |
| console.warn( |
| 'Not inside an iframe...', |
| window.self.origin, |
| window.top?.origin, |
| ); |
| } |
| } |
| |
| allTracesAreDumps(): boolean { |
| for (const trace of this.timelineData.getTraces()) { |
| if (!trace.isDump()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| showCrossToolSyncButton(): boolean { |
| return this.crossToolProtocol.isConnected(); |
| } |
| |
| getCrossToolSyncTooltip(): string { |
| const currStatus = this.crossToolProtocol.getAllowTimestampSync(); |
| |
| return `Cross Tool Sync ${this.translateStatus( |
| currStatus, |
| )} (Click to turn ${this.translateStatus(!currStatus)})`; |
| } |
| |
| onCrossToolSyncButtonClick() { |
| this.crossToolProtocol.setAllowTimestampSync( |
| !this.crossToolProtocol.getAllowTimestampSync(), |
| ); |
| Analytics.Settings.logCrossToolSync( |
| this.crossToolProtocol.getAllowTimestampSync(), |
| ); |
| } |
| |
| getCrossToolSyncButtonColor(): string { |
| return this.crossToolProtocol.getAllowTimestampSync() |
| ? 'primary' |
| : 'accent'; |
| } |
| |
| packetLossWarning(): string | undefined { |
| const lostPackets = this.tracePipeline.lostPackets(); |
| if (lostPackets === 0) { |
| return undefined; |
| } |
| return `${lostPackets} Perfetto packet${ |
| lostPackets > 1 ? 's' : '' |
| } lost during tracing - data may be incomplete`; |
| } |
| |
| async showFileSelectionDialog(filenames: string[]) { |
| await new Promise<void>((resolve) => { |
| this.ngZone.run(() => { |
| const data: WarningDialogData = { |
| message: `Multiple Perfetto traces found. Select one to process:`, |
| actions: [], |
| options: filenames, |
| closeText: 'Process selected trace', |
| singleSelection: true, |
| }; |
| const dialogRef = this.dialog.open(WarningDialogComponent, { |
| data, |
| disableClose: true, |
| panelClass: 'warning-panel', |
| }); |
| dialogRef |
| .beforeClosed() |
| .subscribe(async (result: WarningDialogResult | undefined) => { |
| await this.mediator.onWinscopeEvent( |
| new BugreportFileSelected(result?.selectedOptions[0]), |
| ); |
| resolve(); |
| }); |
| }); |
| }); |
| } |
| |
| private goToLink(url: string) { |
| window.open(url, '_blank'); |
| } |
| |
| private translateStatus(status: boolean) { |
| return status ? 'ON' : 'OFF'; |
| } |
| |
| private downloadTraces(blob: Blob, filename: string) { |
| const url = window.URL.createObjectURL(blob); |
| this.downloadRequest(url, filename); |
| } |
| } |