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 */ 16import { 17 ChangeDetectorRef, 18 Component, 19 EventEmitter, 20 Inject, 21 Input, 22 NgZone, 23 Output, 24} from '@angular/core'; 25import {TracePipeline} from 'app/trace_pipeline'; 26import {ProgressListener} from 'messaging/progress_listener'; 27import {WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event'; 28import {WinscopeEventListener} from 'messaging/winscope_event_listener'; 29import {Trace} from 'trace/trace'; 30import {TRACE_INFO} from 'trace/trace_info'; 31import {TraceTypeUtils} from 'trace/trace_type'; 32import {LoadProgressComponent} from './load_progress_component'; 33 34@Component({ 35 selector: 'upload-traces', 36 template: ` 37 <mat-card class="upload-card"> 38 <div class="card-header"> 39 <mat-card-title class="title">Upload Traces</mat-card-title> 40 <div 41 *ngIf="!isLoadingFiles && tracePipeline.getTraces().getSize() > 0" 42 class="trace-actions-container"> 43 <button 44 color="primary" 45 mat-raised-button 46 class="load-btn" 47 matTooltip="Upload trace with an associated viewer to visualize" 48 [matTooltipDisabled]="hasLoadedFilesWithViewers()" 49 [disabled]="isViewTracesButtonDisabled()" 50 (click)="onViewTracesButtonClick()"> 51 View traces 52 </button> 53 54 <button 55 class="download-btn" 56 color="primary" 57 mat-stroked-button 58 (click)="downloadTracesClick.emit()">Download all</button> 59 60 <button 61 class="upload-btn" 62 color="primary" 63 mat-stroked-button 64 for="fileDropRef" 65 [disabled]="viewersLoading" 66 (click)="fileDropRef.click()"> 67 Upload another file 68 </button> 69 70 <button 71 class="clear-all-btn" 72 color="primary" 73 mat-stroked-button 74 [disabled]="viewersLoading" 75 (click)="onClearButtonClick()"> 76 Clear all 77 </button> 78 </div> 79 </div> 80 81 <mat-card-content 82 class="drop-box" 83 ref="drop-box" 84 (dragleave)="onFileDragOut($event)" 85 (dragover)="onFileDragIn($event)" 86 (drop)="onFileDrop($event)" 87 (click)="fileDropRef.click()"> 88 <input 89 id="fileDropRef" 90 hidden 91 type="file" 92 multiple 93 onclick="this.value = null" 94 #fileDropRef 95 (change)="onInputFiles($event)" /> 96 97 <load-progress 98 *ngIf="isLoadingFiles" 99 [progressPercentage]="progressPercentage" 100 [message]="progressMessage"> 101 </load-progress> 102 103 <mat-list 104 *ngIf="!isLoadingFiles && tracePipeline.getTraces().getSize() > 0" 105 class="uploaded-files"> 106 <mat-list-item 107 [class.no-visualization]="!canVisualizeTrace(trace)" 108 [class.trace-error]="trace.isCorrupted()" 109 *ngFor="let trace of tracePipeline.getTraces()"> 110 <mat-icon 111 matListIcon 112 [style]="{color: TRACE_INFO[trace.type].color}"> 113 {{ TRACE_INFO[trace.type].icon }} 114 </mat-icon> 115 116 <p matLine>{{ TRACE_INFO[trace.type].name }}</p> 117 <p matLine *ngFor="let descriptor of trace.getDescriptors()">{{ descriptor }}</p> 118 119 <mat-icon 120 class="warning-icon" 121 *ngIf="!canVisualizeTrace(trace)" 122 [matTooltip]="cannotVisualizeTraceTooltip(trace)">warning</mat-icon> 123 <mat-icon 124 class="error-icon" 125 *ngIf="trace.isCorrupted()" 126 [matTooltip]="traceErrorTooltip(trace)">error</mat-icon> 127 <button 128 mat-icon-button 129 (click)="onRemoveTrace($event, trace)" 130 [disabled]="viewersLoading"> 131 <mat-icon>close</mat-icon> 132 </button> 133 </mat-list-item> 134 </mat-list> 135 136 <div 137 *ngIf="!isLoadingFiles && tracePipeline.getTraces().getSize() === 0" 138 class="drop-info"> 139 <p class="mat-body-3 icon"> 140 <mat-icon inline fontIcon="upload"></mat-icon> 141 </p> 142 <p class="mat-body-1">Drag your .winscope file(s) or click to upload</p> 143 </div> 144 </mat-card-content> 145 </mat-card> 146 `, 147 styles: [ 148 ` 149 .upload-card { 150 height: 100%; 151 display: flex; 152 flex-direction: column; 153 overflow: auto; 154 margin: 10px; 155 padding-top: 0px; 156 } 157 .card-header { 158 justify-content: space-between; 159 align-items: center; 160 display: flex; 161 flex-direction: row; 162 } 163 .title { 164 padding-top: 16px; 165 text-align: center; 166 } 167 .trace-actions-container { 168 display: flex; 169 flex-direction: row; 170 flex-wrap: wrap; 171 gap: 10px; 172 padding: 4px 0px; 173 } 174 .drop-box { 175 display: flex; 176 flex-direction: column; 177 overflow: auto; 178 border: 2px dashed var(--border-color); 179 cursor: pointer; 180 } 181 .uploaded-files { 182 flex: 400px; 183 padding: 0; 184 } 185 .drop-info { 186 flex: 400px; 187 display: flex; 188 flex-direction: column; 189 justify-content: center; 190 align-items: center; 191 pointer-events: none; 192 } 193 .drop-info p { 194 opacity: 0.6; 195 font-size: 1.2rem; 196 } 197 .drop-info .icon { 198 font-size: 3rem; 199 margin: 0; 200 } 201 .div-progress { 202 display: flex; 203 height: 100%; 204 flex-direction: column; 205 justify-content: center; 206 align-content: center; 207 align-items: center; 208 } 209 .div-progress p { 210 opacity: 0.6; 211 } 212 .div-progress mat-icon { 213 font-size: 3rem; 214 width: unset; 215 height: unset; 216 } 217 .div-progress mat-progress-bar { 218 max-width: 250px; 219 } 220 mat-card-content { 221 flex-grow: 1; 222 } 223 .no-visualization { 224 background-color: var(--warning-background-color); 225 } 226 .trace-error { 227 background-color: var(--error-background-color); 228 } 229 .warning-icon, .error-icon { 230 flex-shrink: 0; 231 } 232 `, 233 ], 234}) 235export class UploadTracesComponent 236 implements WinscopeEventListener, ProgressListener 237{ 238 TRACE_INFO = TRACE_INFO; 239 isLoadingFiles = false; 240 progressMessage = ''; 241 progressPercentage?: number; 242 lastUiProgressUpdateTimeMs?: number; 243 viewersLoading = false; 244 245 @Input() tracePipeline: TracePipeline | undefined; 246 @Output() filesUploaded = new EventEmitter<File[]>(); 247 @Output() viewTracesButtonClick = new EventEmitter<void>(); 248 @Output() downloadTracesClick = new EventEmitter<void>(); 249 250 constructor( 251 @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef, 252 @Inject(NgZone) private ngZone: NgZone, 253 ) {} 254 255 ngOnInit() { 256 this.tracePipeline?.clear(); 257 } 258 259 async onWinscopeEvent(event: WinscopeEvent) { 260 await event.visit(WinscopeEventType.APP_TRACE_VIEW_REQUEST, async () => { 261 this.viewersLoading = true; 262 }); 263 await event.visit( 264 WinscopeEventType.APP_TRACE_VIEW_REQUEST_HANDLED, 265 async () => { 266 this.viewersLoading = false; 267 }, 268 ); 269 } 270 271 onProgressUpdate( 272 message: string | undefined, 273 progressPercentage: number | undefined, 274 ) { 275 if ( 276 !LoadProgressComponent.canUpdateComponent(this.lastUiProgressUpdateTimeMs) 277 ) { 278 return; 279 } 280 this.isLoadingFiles = true; 281 this.progressMessage = message ? message : 'Loading...'; 282 this.progressPercentage = progressPercentage; 283 this.lastUiProgressUpdateTimeMs = Date.now(); 284 this.changeDetectorRef.detectChanges(); 285 } 286 287 onOperationFinished() { 288 this.isLoadingFiles = false; 289 this.lastUiProgressUpdateTimeMs = undefined; 290 this.changeDetectorRef.detectChanges(); 291 } 292 293 onInputFiles(event: Event) { 294 if (this.viewersLoading) { 295 return; 296 } 297 const files = this.getInputFiles(event); 298 if (files.length === 0) return; 299 this.filesUploaded.emit(files); 300 } 301 302 onViewTracesButtonClick() { 303 this.viewTracesButtonClick.emit(); 304 } 305 306 onClearButtonClick() { 307 this.tracePipeline?.clear(); 308 this.onOperationFinished(); 309 } 310 311 onFileDragIn(e: DragEvent) { 312 e.preventDefault(); 313 e.stopPropagation(); 314 } 315 316 onFileDragOut(e: DragEvent) { 317 e.preventDefault(); 318 e.stopPropagation(); 319 } 320 321 onFileDrop(e: DragEvent) { 322 if (this.viewersLoading) { 323 return; 324 } 325 e.preventDefault(); 326 e.stopPropagation(); 327 const droppedFiles = e.dataTransfer?.files; 328 if (!droppedFiles) return; 329 this.filesUploaded.emit(Array.from(droppedFiles)); 330 } 331 332 onRemoveTrace(event: MouseEvent, trace: Trace<object>) { 333 event.preventDefault(); 334 event.stopPropagation(); 335 this.tracePipeline?.removeTrace(trace); 336 this.onOperationFinished(); 337 } 338 339 hasLoadedFilesWithViewers(): boolean { 340 return this.ngZone.run(() => { 341 let hasFilesWithViewers = false; 342 this.tracePipeline?.getTraces().forEachTrace((trace) => { 343 if ( 344 !trace.isCorrupted() && 345 TraceTypeUtils.isTraceTypeWithViewer(trace.type) 346 ) { 347 hasFilesWithViewers = true; 348 } 349 }); 350 351 return hasFilesWithViewers; 352 }); 353 } 354 355 isViewTracesButtonDisabled(): boolean { 356 return this.viewersLoading || !this.hasLoadedFilesWithViewers(); 357 } 358 359 canVisualizeTrace(trace: Trace<object>): boolean { 360 return TraceTypeUtils.isTraceTypeWithViewer(trace.type); 361 } 362 363 cannotVisualizeTraceTooltip(trace: Trace<object>): string { 364 return TraceTypeUtils.getReasonForNoTraceVisualization(trace.type); 365 } 366 367 traceErrorTooltip(trace: Trace<object>): string { 368 const reason = trace.getCorruptedReason() ?? 'Trace is corrupted.'; 369 return 'Cannot visualize trace. ' + reason; 370 } 371 372 private getInputFiles(event: Event): File[] { 373 const files: FileList | null = (event?.target as HTMLInputElement)?.files; 374 if (!files || !files[0]) { 375 return []; 376 } 377 return Array.from(files); 378 } 379} 380