/* * 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 { ChangeDetectorRef, Component, EventEmitter, Inject, Input, NgZone, Output, } from '@angular/core'; import {TracePipeline} from 'app/trace_pipeline'; import {ProgressListener} from 'messaging/progress_listener'; import {WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event'; import {WinscopeEventListener} from 'messaging/winscope_event_listener'; import {Trace} from 'trace/trace'; import {TRACE_INFO} from 'trace/trace_info'; import {TraceTypeUtils} from 'trace/trace_type'; import {LoadProgressComponent} from './load_progress_component'; @Component({ selector: 'upload-traces', template: `
Upload Traces
{{ TRACE_INFO[trace.type].icon }}

{{ TRACE_INFO[trace.type].name }}

{{ descriptor }}

warning error

Drag your .winscope file(s) or click to upload

`, styles: [ ` .upload-card { height: 100%; display: flex; flex-direction: column; overflow: auto; margin: 10px; padding-top: 0px; } .card-header { justify-content: space-between; align-items: center; display: flex; flex-direction: row; } .title { padding-top: 16px; text-align: center; } .trace-actions-container { display: flex; flex-direction: row; flex-wrap: wrap; gap: 10px; padding: 4px 0px; } .drop-box { display: flex; flex-direction: column; overflow: auto; border: 2px dashed var(--border-color); cursor: pointer; } .uploaded-files { flex: 400px; padding: 0; } .drop-info { flex: 400px; display: flex; flex-direction: column; justify-content: center; align-items: center; pointer-events: none; } .drop-info p { opacity: 0.6; font-size: 1.2rem; } .drop-info .icon { font-size: 3rem; margin: 0; } .div-progress { display: flex; height: 100%; flex-direction: column; justify-content: center; align-content: center; align-items: center; } .div-progress p { opacity: 0.6; } .div-progress mat-icon { font-size: 3rem; width: unset; height: unset; } .div-progress mat-progress-bar { max-width: 250px; } mat-card-content { flex-grow: 1; } .no-visualization { background-color: var(--warning-background-color); } .trace-error { background-color: var(--error-background-color); } .warning-icon, .error-icon { flex-shrink: 0; } `, ], }) export class UploadTracesComponent implements WinscopeEventListener, ProgressListener { TRACE_INFO = TRACE_INFO; isLoadingFiles = false; progressMessage = ''; progressPercentage?: number; lastUiProgressUpdateTimeMs?: number; viewersLoading = false; @Input() tracePipeline: TracePipeline | undefined; @Output() filesUploaded = new EventEmitter(); @Output() viewTracesButtonClick = new EventEmitter(); @Output() downloadTracesClick = new EventEmitter(); constructor( @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef, @Inject(NgZone) private ngZone: NgZone, ) {} ngOnInit() { this.tracePipeline?.clear(); } async onWinscopeEvent(event: WinscopeEvent) { await event.visit(WinscopeEventType.APP_TRACE_VIEW_REQUEST, async () => { this.viewersLoading = true; }); await event.visit( WinscopeEventType.APP_TRACE_VIEW_REQUEST_HANDLED, async () => { this.viewersLoading = false; }, ); } onProgressUpdate( message: string | undefined, progressPercentage: number | undefined, ) { if ( !LoadProgressComponent.canUpdateComponent(this.lastUiProgressUpdateTimeMs) ) { return; } this.isLoadingFiles = true; this.progressMessage = message ? message : 'Loading...'; this.progressPercentage = progressPercentage; this.lastUiProgressUpdateTimeMs = Date.now(); this.changeDetectorRef.detectChanges(); } onOperationFinished() { this.isLoadingFiles = false; this.lastUiProgressUpdateTimeMs = undefined; this.changeDetectorRef.detectChanges(); } onInputFiles(event: Event) { if (this.viewersLoading) { return; } const files = this.getInputFiles(event); if (files.length === 0) return; this.filesUploaded.emit(files); } onViewTracesButtonClick() { this.viewTracesButtonClick.emit(); } onClearButtonClick() { this.tracePipeline?.clear(); this.onOperationFinished(); } onFileDragIn(e: DragEvent) { e.preventDefault(); e.stopPropagation(); } onFileDragOut(e: DragEvent) { e.preventDefault(); e.stopPropagation(); } onFileDrop(e: DragEvent) { if (this.viewersLoading) { return; } e.preventDefault(); e.stopPropagation(); const droppedFiles = e.dataTransfer?.files; if (!droppedFiles) return; this.filesUploaded.emit(Array.from(droppedFiles)); } onRemoveTrace(event: MouseEvent, trace: Trace) { event.preventDefault(); event.stopPropagation(); this.tracePipeline?.removeTrace(trace); this.onOperationFinished(); } hasLoadedFilesWithViewers(): boolean { return this.ngZone.run(() => { let hasFilesWithViewers = false; this.tracePipeline?.getTraces().forEachTrace((trace) => { if ( !trace.isCorrupted() && TraceTypeUtils.isTraceTypeWithViewer(trace.type) ) { hasFilesWithViewers = true; } }); return hasFilesWithViewers; }); } isViewTracesButtonDisabled(): boolean { return this.viewersLoading || !this.hasLoadedFilesWithViewers(); } canVisualizeTrace(trace: Trace): boolean { return TraceTypeUtils.isTraceTypeWithViewer(trace.type); } cannotVisualizeTraceTooltip(trace: Trace): string { return TraceTypeUtils.getReasonForNoTraceVisualization(trace.type); } traceErrorTooltip(trace: Trace): string { const reason = trace.getCorruptedReason() ?? 'Trace is corrupted.'; return 'Cannot visualize trace. ' + reason; } private getInputFiles(event: Event): File[] { const files: FileList | null = (event?.target as HTMLInputElement)?.files; if (!files || !files[0]) { return []; } return Array.from(files); } }