diff --git a/tools/winscope-ng/src/app/app.module.ts b/tools/winscope-ng/src/app/app.module.ts index 076e644e5..2dcc73f8b 100644 --- a/tools/winscope-ng/src/app/app.module.ts +++ b/tools/winscope-ng/src/app/app.module.ts @@ -1,5 +1,6 @@ import { NgModule } from "@angular/core"; import { BrowserModule } from "@angular/platform-browser"; +import { DragDropModule } from "@angular/cdk/drag-drop"; import { ScrollingModule } from "@angular/cdk/scrolling"; import { CommonModule } from "@angular/common"; import { MatCardModule } from "@angular/material/card"; @@ -46,6 +47,7 @@ import { TreeNodeDataViewComponent } from "viewers/components/tree_node_data_vie import { TreeNodePropertiesDataViewComponent } from "viewers/components/tree_node_properties_data_view.component"; import { ViewerInputMethodComponent } from "viewers/components/viewer_input_method.component"; import { ViewerProtologComponent} from "viewers/viewer_protolog/viewer_protolog.component"; +import { ViewerScreenRecordingComponent } from "viewers/viewer_screen_recording/viewer_screen_recording.component"; import { ViewerSurfaceFlingerComponent } from "viewers/viewer_surface_flinger/viewer_surface_flinger.component"; import { ViewerTransactionsComponent } from "viewers/viewer_transactions/viewer_transactions.component"; import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/viewer_window_manager.component"; @@ -58,6 +60,7 @@ import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/view ViewerInputMethodComponent, ViewerProtologComponent, ViewerTransactionsComponent, + ViewerScreenRecordingComponent, CollectTracesComponent, UploadTracesComponent, AdbProxyComponent, @@ -103,6 +106,7 @@ import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/view MatTabsModule, MatSnackBarModule, ScrollingModule, + DragDropModule, ], bootstrap: [AppComponent] }) diff --git a/tools/winscope-ng/src/app/components/app.component.ts b/tools/winscope-ng/src/app/components/app.component.ts index 43505289e..64436cc0f 100644 --- a/tools/winscope-ng/src/app/components/app.component.ts +++ b/tools/winscope-ng/src/app/components/app.component.ts @@ -19,12 +19,15 @@ import { MatSliderChange } from "@angular/material/slider"; import { TraceCoordinator } from "app/trace_coordinator"; import { PersistentStore } from "common/persistent_store"; import { Timestamp } from "common/trace/timestamp"; +import { FileUtils } from "common/utils/file_utils"; import { proxyClient, ProxyState } from "trace_collection/proxy_client"; import { ViewerInputMethodComponent } from "viewers/components/viewer_input_method.component"; +import { Viewer } from "viewers/viewer"; import { ViewerProtologComponent} from "viewers/viewer_protolog/viewer_protolog.component"; import { ViewerSurfaceFlingerComponent } from "viewers/viewer_surface_flinger/viewer_surface_flinger.component"; import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/viewer_window_manager.component"; import { ViewerTransactionsComponent } from "viewers/viewer_transactions/viewer_transactions.component"; +import { ViewerScreenRecordingComponent } from "viewers/viewer_screen_recording/viewer_screen_recording.component"; @Component({ selector: "app-root", @@ -51,8 +54,9 @@ import { ViewerTransactionsComponent } from "viewers/viewer_transactions/viewer_
@@ -129,6 +133,7 @@ export class AppComponent { currentTimestamp?: Timestamp; currentTimestampIndex = 0; allTimestamps: Timestamp[] = []; + allViewers: Viewer[] = []; @Input() dataLoaded = false; constructor( @@ -144,6 +149,10 @@ export class AppComponent { customElements.define("viewer-protolog", createCustomElement(ViewerProtologComponent, {injector})); } + if (!customElements.get("viewer-screen-recording")) { + customElements.define("viewer-screen-recording", + createCustomElement(ViewerScreenRecordingComponent, {injector})); + } if (!customElements.get("viewer-surface-flinger")) { customElements.define("viewer-surface-flinger", createCustomElement(ViewerSurfaceFlingerComponent, {injector})); @@ -182,8 +191,9 @@ export class AppComponent { public onDataLoadedChange(dataLoaded: boolean) { if (dataLoaded && !(this.traceCoordinator.getViewers().length > 0)) { - this.allTimestamps = this.traceCoordinator.getTimestamps(); this.traceCoordinator.createViewers(); + this.allViewers = this.traceCoordinator.getViewers(); + this.allTimestamps = this.traceCoordinator.getTimestamps(); this.currentTimestampIndex = 0; this.notifyCurrentTimestamp(); this.dataLoaded = dataLoaded; @@ -194,4 +204,18 @@ export class AppComponent { this.currentTimestamp = this.allTimestamps[this.currentTimestampIndex]; this.traceCoordinator.notifyCurrentTimestamp(this.currentTimestamp); } + + private async onDownloadTracesButtonClick() { + const traces = await this.traceCoordinator.getAllTracesForDownload(); + const zipFileBlob = await FileUtils.createZipArchive(traces); + const zipFileName = "winscope.zip"; + const a = document.createElement("a"); + document.body.appendChild(a); + const url = window.URL.createObjectURL(zipFileBlob); + a.href = url; + a.download = zipFileName; + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + } } diff --git a/tools/winscope-ng/src/app/components/trace_view.component.spec.ts b/tools/winscope-ng/src/app/components/trace_view.component.spec.ts index ca951077d..36140b8f2 100644 --- a/tools/winscope-ng/src/app/components/trace_view.component.spec.ts +++ b/tools/winscope-ng/src/app/components/trace_view.component.spec.ts @@ -13,12 +13,35 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { CommonModule } from "@angular/common"; -import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { TraceViewComponent } from "./trace_view.component"; -import { MatCardModule } from "@angular/material/card"; -import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from "@angular/core"; -import { TraceCoordinator } from "app/trace_coordinator"; +import {CommonModule} from "@angular/common"; +import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA} from "@angular/core"; +import {ComponentFixture, TestBed} from "@angular/core/testing"; +import {MatCardModule} from "@angular/material/card"; +import {TraceViewComponent} from "./trace_view.component"; +import {View, Viewer, ViewType} from "viewers/viewer"; + +class FakeViewer implements Viewer { + constructor(title: string, content: string) { + this.title = title; + this.htmlElement = document.createElement("div"); + this.htmlElement.innerText = content; + } + + notifyCurrentTraceEntries(entries: any) { + // do nothing + } + + getViews(): View[] { + return [new View(ViewType.TAB, this.htmlElement, this.title)]; + } + + getDependencies(): any[] { + return []; + } + + private htmlElement: HTMLElement; + private title: string; +} describe("TraceViewComponent", () => { let fixture: ComponentFixture; @@ -27,27 +50,22 @@ describe("TraceViewComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ + declarations: [TraceViewComponent], imports: [ CommonModule, MatCardModule ], - declarations: [TraceViewComponent], schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA] }).compileComponents(); fixture = TestBed.createComponent(TraceViewComponent); - component = fixture.componentInstance; - component.traceCoordinator = new TraceCoordinator(); - component.viewerTabs = [ - { - label: "Surface Flinger", - cardId: 0, - }, - { - label: "Window Manager", - cardId: 1, - } - ]; htmlElement = fixture.nativeElement; + component = fixture.componentInstance; + component.viewers = [ + new FakeViewer("Title0", "Content0"), + new FakeViewer("Title1", "Content1") + ]; + component.ngOnChanges(); + fixture.detectChanges(); }); it("can be created", () => { @@ -56,35 +74,57 @@ describe("TraceViewComponent", () => { }); it("creates viewer tabs", () => { - fixture.detectChanges(); - const tabs = htmlElement.querySelectorAll(".viewer-tab"); + const tabs: NodeList = htmlElement.querySelectorAll(".viewer-tab"); expect(tabs.length).toEqual(2); - expect(component.activeViewerCardId).toEqual(0); + expect(tabs.item(0)!.textContent).toEqual("Title0"); + expect(tabs.item(1)!.textContent).toEqual("Title1"); }); - it("changes active viewer on click", async () => { - fixture.detectChanges(); - expect(component.activeViewerCardId).toEqual(0); + it("changes active viewer on click", () => { const tabs = htmlElement.querySelectorAll(".viewer-tab"); - tabs[0].dispatchEvent(new Event("click")); + const tabsContent = + htmlElement.querySelectorAll(".trace-view-content div"); + + // Initially tab 0 fixture.detectChanges(); - await fixture.whenStable(); - const firstId = component.activeViewerCardId; + expect(tabsContent.length).toEqual(2); + expect(tabsContent[0].innerHTML).toEqual("Content0"); + expect(tabsContent[1].innerHTML).toEqual("Content1"); + expect((tabsContent[0]).style?.display).toEqual(""); + expect((tabsContent[1]).style?.display).toEqual("none"); + + // Switch to tab 1 tabs[1].dispatchEvent(new Event("click")); fixture.detectChanges(); - await fixture.whenStable(); - const secondId = component.activeViewerCardId; - expect(firstId !== secondId).toBeTrue; + expect(tabsContent.length).toEqual(2); + expect(tabsContent[0].innerHTML).toEqual("Content0"); + expect(tabsContent[1].innerHTML).toEqual("Content1"); + expect((tabsContent[0]).style?.display).toEqual("none"); + expect((tabsContent[1]).style?.display).toEqual(""); + + // Switch to tab 0 + tabs[0].dispatchEvent(new Event("click")); + fixture.detectChanges(); + expect(tabsContent.length).toEqual(2); + expect(tabsContent[0].innerHTML).toEqual("Content0"); + expect(tabsContent[1].innerHTML).toEqual("Content1"); + expect((tabsContent[0]).style?.display).toEqual(""); + expect((tabsContent[1]).style?.display).toEqual("none"); }); - it("downloads all traces", async () => { - spyOn(component, "downloadAllTraces").and.callThrough(); - fixture.detectChanges(); - const downloadButton: HTMLButtonElement | null = htmlElement.querySelector(".save-btn"); + it("emits event on download button click", () => { + const spy = spyOn(component.downloadTracesButtonClick, "emit"); + + const downloadButton: null|HTMLButtonElement = + htmlElement.querySelector(".save-btn"); expect(downloadButton).toBeInstanceOf(HTMLButtonElement); + downloadButton?.dispatchEvent(new Event("click")); fixture.detectChanges(); - await fixture.whenStable(); - expect(component.downloadAllTraces).toHaveBeenCalled(); + expect(spy).toHaveBeenCalledTimes(1); + + downloadButton?.dispatchEvent(new Event("click")); + fixture.detectChanges(); + expect(spy).toHaveBeenCalledTimes(2); }); }); diff --git a/tools/winscope-ng/src/app/components/trace_view.component.ts b/tools/winscope-ng/src/app/components/trace_view.component.ts index 665322554..f3dd41345 100644 --- a/tools/winscope-ng/src/app/components/trace_view.component.ts +++ b/tools/winscope-ng/src/app/components/trace_view.component.ts @@ -13,20 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - Component, - Input, - Inject, - ElementRef, -} from "@angular/core"; -import { TraceCoordinator } from "app/trace_coordinator"; -import { PersistentStore } from "common/persistent_store"; -import { FileUtils } from "common/utils/file_utils"; -import { Viewer } from "viewers/viewer"; +import {Component, ElementRef, EventEmitter, Inject, Input, Output} from "@angular/core"; +import {PersistentStore} from "common/persistent_store"; +import {Viewer, View, ViewType} from "viewers/viewer"; @Component({ selector: "trace-view", template: ` +
+
@@ -49,6 +44,16 @@ import { Viewer } from "viewers/viewer"; `, styles: [ ` + .container-overlay { + z-index: 10; + position: fixed; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + pointer-events: none; + } + .header-items-wrapper { width: 100%; display: flex; @@ -73,45 +78,21 @@ import { Viewer } from "viewers/viewer"; ] }) export class TraceViewComponent { + @Input() viewers!: Viewer[]; @Input() store!: PersistentStore; - @Input() traceCoordinator!: TraceCoordinator; - viewerTabs: ViewerTab[] = []; - activeViewerCardId = 0; - views: HTMLElement[] = []; + @Output() downloadTracesButtonClick = new EventEmitter(); - constructor( - @Inject(ElementRef) private elementRef: ElementRef, - ) {} + private elementRef: ElementRef; + private viewerTabs: ViewerTab[] = []; + private activeViewerCardId = 0; - ngDoCheck() { - if (this.traceCoordinator.getViewers().length > 0 && !this.viewersAdded()) { - let cardCounter = 0; - this.activeViewerCardId = 0; - this.viewerTabs = []; - this.traceCoordinator.getViewers().forEach((viewer: Viewer) => { - // create tab for viewer nav bar - const tab = { - label: viewer.getTitle(), - cardId: cardCounter, - }; - this.viewerTabs.push(tab); + constructor(@Inject(ElementRef) elementRef: ElementRef) { + this.elementRef = elementRef; + } - // add properties to view and add view to trace view card - const view = viewer.getView(); - (view as any).store = this.store; - view.id = `card-${cardCounter}`; - view.style.display = this.isActiveViewerCard(cardCounter) ? "" : "none"; - - const traceViewContent = this.elementRef.nativeElement.querySelector(".trace-view-content")!; - traceViewContent.appendChild(view); - this.views.push(view); - cardCounter++; - }); - } else if (this.traceCoordinator.getViewers().length === 0 && this.viewersAdded()) { - this.activeViewerCardId = 0; - this.views.forEach(view => view.remove()); - this.views = []; - } + ngOnChanges() { + this.renderViewsTab(); + this.renderViewsOverlay(); } public showViewer(cardId: number) { @@ -124,22 +105,56 @@ export class TraceViewComponent { return this.activeViewerCardId === cardId; } - public async downloadAllTraces() { - const traces = await this.traceCoordinator.getAllTracesForDownload(); - const zipFileBlob = await FileUtils.createZipArchive(traces); - const zipFileName = "winscope.zip"; - const a = document.createElement("a"); - document.body.appendChild(a); - const url = window.URL.createObjectURL(zipFileBlob); - a.href = url; - a.download = zipFileName; - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); + private renderViewsTab() { + this.activeViewerCardId = 0; + this.viewerTabs = []; + + const views: View[] = this.viewers + .map(viewer => viewer.getViews()) + .flat() + .filter(view => (view.type === ViewType.TAB)); + + for (const [cardCounter, view] of views.entries()) { + if (!view) { + continue; + } + + // create tab for viewer nav bar + const tab = { + label: view.title, + cardId: cardCounter, + }; + this.viewerTabs.push(tab); + + // add properties to view and add view to trace view card + (view as any).store = this.store; + view.htmlElement.id = `card-${cardCounter}`; + view.htmlElement.style.display = this.isActiveViewerCard(cardCounter) ? "" : "none"; + + const traceViewContent = this.elementRef.nativeElement.querySelector(".trace-view-content")!; + traceViewContent.appendChild(view.htmlElement); + } } - private viewersAdded() { - return this.views.length > 0; + private renderViewsOverlay() { + const views: View[] = this.viewers + .map(viewer => viewer.getViews()) + .flat() + .filter(view => (view.type === ViewType.OVERLAY)); + + views.forEach(view => { + view.htmlElement.style.pointerEvents = "all"; + view.htmlElement.style.position = "absolute"; + view.htmlElement.style.bottom = "10%"; + view.htmlElement.style.right = "0px"; + + const containerOverlay = this.elementRef.nativeElement.querySelector(".container-overlay"); + if (!containerOverlay) { + throw new Error("Failed to find overlay container sub-element"); + } + + containerOverlay!.appendChild(view.htmlElement); + }); } private isActiveViewerCard(cardId: number) { diff --git a/tools/winscope-ng/src/app/trace_coordinator.ts b/tools/winscope-ng/src/app/trace_coordinator.ts index ecb0b92ce..17c44ab2a 100644 --- a/tools/winscope-ng/src/app/trace_coordinator.ts +++ b/tools/winscope-ng/src/app/trace_coordinator.ts @@ -38,7 +38,6 @@ class TraceCoordinator { traces = this.parsers.map(parser => parser.getTrace()).concat(traces); let parserErrors: ParserError[]; [this.parsers, parserErrors] = await new ParserFactory().createParsers(traces); - console.log("created parsers: ", this.parsers); return parserErrors; } diff --git a/tools/winscope-ng/src/parsers/parser_screen_recording.spec.ts b/tools/winscope-ng/src/parsers/parser_screen_recording.spec.ts index 8dfff7714..6c152a0e4 100644 --- a/tools/winscope-ng/src/parsers/parser_screen_recording.spec.ts +++ b/tools/winscope-ng/src/parsers/parser_screen_recording.spec.ts @@ -23,7 +23,7 @@ describe("ParserScreenRecording", () => { let parser: Parser; beforeAll(async () => { - parser = await UnitTestUtils.getParser("traces/elapsed_and_real_timestamp/screen_recording.mp4"); + parser = await UnitTestUtils.getParser("traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4"); }); it("has expected trace type", () => { @@ -32,14 +32,15 @@ describe("ParserScreenRecording", () => { it ("provides elapsed timestamps", () => { const timestamps = parser.getTimestamps(TimestampType.ELAPSED)!; + console.log(timestamps[timestamps.length-1]); expect(timestamps.length) - .toEqual(15); + .toEqual(123); const expected = [ - new Timestamp(TimestampType.ELAPSED, 144857685000n), - new Timestamp(TimestampType.ELAPSED, 144866679000n), - new Timestamp(TimestampType.ELAPSED, 144875772000n), + new Timestamp(TimestampType.ELAPSED, 211827840430n), + new Timestamp(TimestampType.ELAPSED, 211842401430n), + new Timestamp(TimestampType.ELAPSED, 211862172430n), ]; expect(timestamps.slice(0, 3)) .toEqual(expected); @@ -49,12 +50,12 @@ describe("ParserScreenRecording", () => { const timestamps = parser.getTimestamps(TimestampType.REAL)!; expect(timestamps.length) - .toEqual(15); + .toEqual(123); const expected = [ - new Timestamp(TimestampType.REAL, 1659687791485257266n), - new Timestamp(TimestampType.REAL, 1659687791494251266n), - new Timestamp(TimestampType.REAL, 1659687791503344266n), + new Timestamp(TimestampType.REAL, 1666361048792787045n), + new Timestamp(TimestampType.REAL, 1666361048807348045n), + new Timestamp(TimestampType.REAL, 1666361048827119045n), ]; expect(timestamps.slice(0, 3)) .toEqual(expected); @@ -62,33 +63,33 @@ describe("ParserScreenRecording", () => { it("retrieves trace entry from elapsed timestamp", () => { { - const timestamp = new Timestamp(TimestampType.ELAPSED, 144857685000n); + const timestamp = new Timestamp(TimestampType.ELAPSED, 211827840430n); const entry = parser.getTraceEntry(timestamp)!; expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry); expect(Number(entry.videoTimeSeconds)).toBeCloseTo(0); } { - const timestamp = new Timestamp(TimestampType.ELAPSED, 145300550000n); + const timestamp = new Timestamp(TimestampType.ELAPSED, 213198917430n); const entry = parser.getTraceEntry(timestamp)!; expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry); - expect(Number(entry.videoTimeSeconds)).toBeCloseTo(0.442, 0.001); + expect(Number(entry.videoTimeSeconds)).toBeCloseTo(1.371077000, 0.001); } }); it("retrieves trace entry from real timestamp", () => { { - const timestamp = new Timestamp(TimestampType.REAL, 1659687791485257266n); + const timestamp = new Timestamp(TimestampType.REAL, 1666361048792787045n); const entry = parser.getTraceEntry(timestamp)!; expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry); expect(Number(entry.videoTimeSeconds)).toBeCloseTo(0); } { - const timestamp = new Timestamp(TimestampType.REAL, 1659687791928122266n); + const timestamp = new Timestamp(TimestampType.REAL, 1666361050163864045n); const entry = parser.getTraceEntry(timestamp)!; expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry); - expect(Number(entry.videoTimeSeconds)).toBeCloseTo(0.322, 0.001); + expect(Number(entry.videoTimeSeconds)).toBeCloseTo(1.371077000, 0.001); } }); }); diff --git a/tools/winscope-ng/src/parsers/parser_screen_recording.ts b/tools/winscope-ng/src/parsers/parser_screen_recording.ts index ed7f5356c..54e776a44 100644 --- a/tools/winscope-ng/src/parsers/parser_screen_recording.ts +++ b/tools/winscope-ng/src/parsers/parser_screen_recording.ts @@ -20,7 +20,7 @@ import {Parser} from "./parser"; import {ScreenRecordingTraceEntry} from "common/trace/screen_recording"; class ScreenRecordingMetadataEntry { - constructor(public timestampMonotonicNs: bigint, public timestampRealtimeNs: bigint) { + constructor(public timestampElapsedNs: bigint, public timestampRealtimeNs: bigint) { } } @@ -40,15 +40,31 @@ class ParserScreenRecording extends Parser { override decodeTrace(videoData: Uint8Array): ScreenRecordingMetadataEntry[] { const posVersion = this.searchMagicString(videoData); const [posTimeOffset, metadataVersion] = this.parseMetadataVersion(videoData, posVersion); - if (metadataVersion !== 1) { + + if (metadataVersion !== 1 && metadataVersion !== 2) { throw TypeError(`Metadata version "${metadataVersion}" not supported`); } - const [posCount, timeOffsetNs] = this.parseRealToMonotonicTimeOffsetNs(videoData, posTimeOffset); - const [posTimestamps, count] = this.parseFramesCount(videoData, posCount); - const timestampsMonotonicNs = this.parseTimestampsMonotonicNs(videoData, posTimestamps, count); - return timestampsMonotonicNs.map((timestampMonotonicNs: bigint) => { - return new ScreenRecordingMetadataEntry(timestampMonotonicNs, timestampMonotonicNs + timeOffsetNs); + if (metadataVersion === 1) { + // UI traces contain "elapsed" timestamps (SYSTEM_TIME_BOOTTIME), whereas + // metadata Version 1 contains SYSTEM_TIME_MONOTONIC timestamps. + // + // Here we are pretending that metadata Version 1 contains "elapsed" + // timestamps as well, in order to synchronize with the other traces. + // + // If no device suspensions are involved, SYSTEM_TIME_MONOTONIC should + // indeed correspond to SYSTEM_TIME_BOOTTIME and things will work as + // expected. + console.warn(`Screen recording may not be synchronized with the + other traces. Metadata contains monotonic time instead of elapsed.`); + } + + const [posCount, timeOffsetNs] = this.parseRealToElapsedTimeOffsetNs(videoData, posTimeOffset); + const [posTimestamps, count] = this.parseFramesCount(videoData, posCount); + const timestampsElapsedNs = this.parseTimestampsElapsedNs(videoData, posTimestamps, count); + + return timestampsElapsedNs.map((timestampElapsedNs: bigint) => { + return new ScreenRecordingMetadataEntry(timestampElapsedNs, timestampElapsedNs + timeOffsetNs); }); } @@ -57,15 +73,7 @@ class ParserScreenRecording extends Parser { return undefined; } if (type === TimestampType.ELAPSED) { - // Traces typically contain "elapsed" timestamps (SYSTEM_TIME_BOOTTIME), - // whereas screen recordings contain SYSTEM_TIME_MONOTONIC timestamps. - // - // Here we are pretending that screen recordings contain "elapsed" timestamps - // as well, in order to synchronize with the other traces. - // - // If no device suspensions are involved, SYSTEM_TIME_MONOTONIC should indeed - // correspond to SYSTEM_TIME_BOOTTIME and things will work as expected. - return new Timestamp(type, decodedEntry.timestampMonotonicNs); + return new Timestamp(type, decodedEntry.timestampElapsedNs); } else if (type === TimestampType.REAL) { return new Timestamp(type, decodedEntry.timestampRealtimeNs); @@ -75,7 +83,7 @@ class ParserScreenRecording extends Parser { override processDecodedEntry(index: number, entry: ScreenRecordingMetadataEntry): ScreenRecordingTraceEntry { const initialTimestampNs = this.getTimestamps(TimestampType.ELAPSED)![0].getValueNs(); - const currentTimestampNs = entry.timestampMonotonicNs; + const currentTimestampNs = entry.timestampElapsedNs; const videoTimeSeconds = Number(currentTimestampNs - initialTimestampNs) / 1000000000; const videoData = this.trace; return new ScreenRecordingTraceEntry(videoTimeSeconds, videoData); @@ -99,9 +107,9 @@ class ParserScreenRecording extends Parser { return [pos, version]; } - private parseRealToMonotonicTimeOffsetNs(videoData: Uint8Array, pos: number) : [number, bigint] { + private parseRealToElapsedTimeOffsetNs(videoData: Uint8Array, pos: number) : [number, bigint] { if (pos + 8 > videoData.length) { - throw new TypeError("Failed to parse realtime-to-monotonic time offset. Video data is too short."); + throw new TypeError("Failed to parse realtime-to-elapsed time offset. Video data is too short."); } const offset = ArrayUtils.toIntLittleEndian(videoData, pos, pos+8); pos += 8; @@ -117,9 +125,9 @@ class ParserScreenRecording extends Parser { return [pos, count]; } - private parseTimestampsMonotonicNs(videoData: Uint8Array, pos: number, count: number) : bigint[] { - if (pos + count * 16 > videoData.length) { - throw new TypeError("Failed to parse monotonic timestamps. Video data is too short."); + private parseTimestampsElapsedNs(videoData: Uint8Array, pos: number, count: number) : bigint[] { + if (pos + count * 8 > videoData.length) { + throw new TypeError("Failed to parse timestamps. Video data is too short."); } const timestamps: bigint[] = []; for (let i = 0; i < count; ++i) { diff --git a/tools/winscope-ng/src/test/e2e/viewer_screen_recording.spec.ts b/tools/winscope-ng/src/test/e2e/viewer_screen_recording.spec.ts new file mode 100644 index 000000000..4c97c66a6 --- /dev/null +++ b/tools/winscope-ng/src/test/e2e/viewer_screen_recording.spec.ts @@ -0,0 +1,45 @@ +/* + * 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 {browser, element, by} from "protractor"; +import {E2eTestUtils} from "./utils"; + +describe("Viewer ScreenRecording", () => { + beforeAll(async () => { + browser.manage().timeouts().implicitlyWait(1000); + browser.get("file://" + E2eTestUtils.getProductionIndexHtmlPath()); + }), + + it("processes trace and renders view", async () => { + const inputFile = element(by.css("input[type=\"file\"]")); + await inputFile.sendKeys(E2eTestUtils.getFixturePath( + "traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4")); + + const loadData = element(by.css(".load-btn")); + await loadData.click(); + + const viewer = element(by.css("viewer-screen-recording")); + expect(await viewer.isPresent()) + .toBeTruthy(); + + const video = element(by.css("viewer-screen-recording video")); + expect(await video.isPresent()) + .toBeTruthy(); + expect(await video.getAttribute("src")) + .toContain("blob:"); + expect(await video.getAttribute("currentTime")) + .toEqual("0"); + }); +}); diff --git a/tools/winscope-ng/src/test/fixtures/traces/elapsed_and_real_timestamp/screen_recording.mp4 b/tools/winscope-ng/src/test/fixtures/traces/elapsed_and_real_timestamp/screen_recording.mp4 deleted file mode 100644 index 0d28a1d71..000000000 Binary files a/tools/winscope-ng/src/test/fixtures/traces/elapsed_and_real_timestamp/screen_recording.mp4 and /dev/null differ diff --git a/tools/winscope-ng/src/test/fixtures/traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4 b/tools/winscope-ng/src/test/fixtures/traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4 new file mode 100644 index 000000000..a6c43d609 Binary files /dev/null and b/tools/winscope-ng/src/test/fixtures/traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4 differ diff --git a/tools/winscope-ng/src/viewers/common/viewer_input_method.ts b/tools/winscope-ng/src/viewers/common/viewer_input_method.ts index 561eb6253..a2f4ec202 100644 --- a/tools/winscope-ng/src/viewers/common/viewer_input_method.ts +++ b/tools/winscope-ng/src/viewers/common/viewer_input_method.ts @@ -14,14 +14,14 @@ * limitations under the License. */ import {TraceType} from "common/trace/trace_type"; -import {Viewer} from "viewers/viewer"; +import {View, Viewer, ViewType} from "viewers/viewer"; import {ViewerEvents} from "viewers/common/viewer_events"; -import { PresenterInputMethod } from "viewers/common/presenter_input_method"; -import { ImeUiData } from "viewers/common/ime_ui_data"; +import {PresenterInputMethod} from "viewers/common/presenter_input_method"; +import {ImeUiData} from "viewers/common/ime_ui_data"; abstract class ViewerInputMethod implements Viewer { constructor() { - this.view = document.createElement("viewer-input-method"); + this.htmlElement = document.createElement("viewer-input-method"); this.presenter = this.initialisePresenter(); this.addViewerEventListeners(); } @@ -30,35 +30,31 @@ abstract class ViewerInputMethod implements Viewer { this.presenter.notifyCurrentTraceEntries(entries); } - public getView(): HTMLElement { - return this.view; - } - + public abstract getViews(): View[]; + public abstract getDependencies(): TraceType[]; protected imeUiCallback = (uiData: ImeUiData) => { // Angular does not deep watch @Input properties. Clearing inputData to null before repopulating // automatically ensures that the UI will change via the Angular change detection cycle. Without // resetting, Angular does not auto-detect that inputData has changed. - (this.view as any).inputData = null; - (this.view as any).inputData = uiData; + (this.htmlElement as any).inputData = null; + (this.htmlElement as any).inputData = uiData; }; protected addViewerEventListeners() { - this.view.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems(((event as CustomEvent).detail.pinnedItem))); - this.view.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`)); - this.view.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions)); - this.view.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent).detail.filterString)); - this.view.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => this.presenter.updatePropertiesTree((event as CustomEvent).detail.userOptions)); - this.view.addEventListener(ViewerEvents.PropertiesFilterChange, (event) => this.presenter.filterPropertiesTree((event as CustomEvent).detail.filterString)); - this.view.addEventListener(ViewerEvents.SelectedTreeChange, (event) => this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem)); - this.view.addEventListener(ViewerEvents.AdditionalPropertySelected, (event) => this.presenter.newAdditionalPropertiesTree((event as CustomEvent).detail.selectedItem)); + this.htmlElement.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems(((event as CustomEvent).detail.pinnedItem))); + this.htmlElement.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`)); + this.htmlElement.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions)); + this.htmlElement.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent).detail.filterString)); + this.htmlElement.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => this.presenter.updatePropertiesTree((event as CustomEvent).detail.userOptions)); + this.htmlElement.addEventListener(ViewerEvents.PropertiesFilterChange, (event) => this.presenter.filterPropertiesTree((event as CustomEvent).detail.filterString)); + this.htmlElement.addEventListener(ViewerEvents.SelectedTreeChange, (event) => this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem)); + this.htmlElement.addEventListener(ViewerEvents.AdditionalPropertySelected, (event) => this.presenter.newAdditionalPropertiesTree((event as CustomEvent).detail.selectedItem)); } - abstract getDependencies(): TraceType[]; - abstract getTitle(): string; protected abstract initialisePresenter(): PresenterInputMethod; - protected view: HTMLElement; + protected htmlElement: HTMLElement; protected presenter: PresenterInputMethod; } diff --git a/tools/winscope-ng/src/viewers/viewer.ts b/tools/winscope-ng/src/viewers/viewer.ts index 009fa80ba..f47491ff7 100644 --- a/tools/winscope-ng/src/viewers/viewer.ts +++ b/tools/winscope-ng/src/viewers/viewer.ts @@ -15,11 +15,24 @@ */ import { TraceType } from "common/trace/trace_type"; +enum ViewType { + TAB, + OVERLAY +} + +class View { + constructor( + public type: ViewType, + public htmlElement: HTMLElement, + public title: string + ) { + } +} + interface Viewer { notifyCurrentTraceEntries(entries: Map): void; - getView(): HTMLElement; - getTitle(): string; + getViews(): View[]; getDependencies(): TraceType[]; } -export { Viewer }; +export {Viewer, View, ViewType}; diff --git a/tools/winscope-ng/src/viewers/viewer_factory.ts b/tools/winscope-ng/src/viewers/viewer_factory.ts index 65c0687c2..62b5ab47d 100644 --- a/tools/winscope-ng/src/viewers/viewer_factory.ts +++ b/tools/winscope-ng/src/viewers/viewer_factory.ts @@ -22,6 +22,7 @@ import {ViewerProtoLog} from "./viewer_protolog/viewer_protolog"; import {ViewerSurfaceFlinger} from "./viewer_surface_flinger/viewer_surface_flinger"; import {ViewerWindowManager} from "./viewer_window_manager/viewer_window_manager"; import {ViewerTransactions} from "./viewer_transactions/viewer_transactions"; +import {ViewerScreenRecording} from "./viewer_screen_recording/viewer_screen_recording"; class ViewerFactory { static readonly VIEWERS = [ @@ -29,6 +30,7 @@ class ViewerFactory { ViewerInputMethodManagerService, ViewerInputMethodService, ViewerProtoLog, + ViewerScreenRecording, ViewerSurfaceFlinger, ViewerTransactions, ViewerWindowManager, diff --git a/tools/winscope-ng/src/viewers/viewer_input_method_clients/viewer_input_method_clients.ts b/tools/winscope-ng/src/viewers/viewer_input_method_clients/viewer_input_method_clients.ts index f733479f4..0f17234c0 100644 --- a/tools/winscope-ng/src/viewers/viewer_input_method_clients/viewer_input_method_clients.ts +++ b/tools/winscope-ng/src/viewers/viewer_input_method_clients/viewer_input_method_clients.ts @@ -14,19 +14,20 @@ * limitations under the License. */ import {TraceType} from "common/trace/trace_type"; -import { PresenterInputMethodClients } from "./presenter_input_method_clients"; -import { ViewerInputMethod } from "viewers/common/viewer_input_method"; +import {PresenterInputMethodClients} from "./presenter_input_method_clients"; +import {ViewerInputMethod} from "viewers/common/viewer_input_method"; +import {View, ViewType} from "viewers/viewer"; class ViewerInputMethodClients extends ViewerInputMethod { - public getTitle(): string { - return "Input Method Clients"; + override getViews(): View[] { + return [new View(ViewType.TAB, this.htmlElement, "Input Method Clients")]; } - public getDependencies(): TraceType[] { + override getDependencies(): TraceType[] { return ViewerInputMethodClients.DEPENDENCIES; } - protected initialisePresenter() { + override initialisePresenter() { return new PresenterInputMethodClients(this.imeUiCallback, this.getDependencies()); } diff --git a/tools/winscope-ng/src/viewers/viewer_input_method_manager_service/viewer_input_method_manager_service.ts b/tools/winscope-ng/src/viewers/viewer_input_method_manager_service/viewer_input_method_manager_service.ts index 79bbab444..b84b5bc25 100644 --- a/tools/winscope-ng/src/viewers/viewer_input_method_manager_service/viewer_input_method_manager_service.ts +++ b/tools/winscope-ng/src/viewers/viewer_input_method_manager_service/viewer_input_method_manager_service.ts @@ -14,19 +14,24 @@ * limitations under the License. */ import {TraceType} from "common/trace/trace_type"; -import { PresenterInputMethodManagerService } from "./presenter_input_method_manager_service"; -import { ViewerInputMethod } from "viewers/common/viewer_input_method"; +import {PresenterInputMethodManagerService} from "./presenter_input_method_manager_service"; +import {ViewerInputMethod} from "viewers/common/viewer_input_method"; +import {View, ViewType} from "viewers/viewer"; class ViewerInputMethodManagerService extends ViewerInputMethod { - public getTitle(): string { - return "Input Method Manager Service"; + override getViews(): View[] { + return [ + new View(ViewType.TAB, + this.htmlElement, + "Input Method Manager Service") + ]; } - public getDependencies(): TraceType[] { + override getDependencies(): TraceType[] { return ViewerInputMethodManagerService.DEPENDENCIES; } - protected initialisePresenter() { + override initialisePresenter() { return new PresenterInputMethodManagerService(this.imeUiCallback, this.getDependencies()); } diff --git a/tools/winscope-ng/src/viewers/viewer_input_method_service/viewer_input_method_service.ts b/tools/winscope-ng/src/viewers/viewer_input_method_service/viewer_input_method_service.ts index 3d5c1ac6a..0b862313a 100644 --- a/tools/winscope-ng/src/viewers/viewer_input_method_service/viewer_input_method_service.ts +++ b/tools/winscope-ng/src/viewers/viewer_input_method_service/viewer_input_method_service.ts @@ -14,19 +14,26 @@ * limitations under the License. */ import {TraceType} from "common/trace/trace_type"; -import { PresenterInputMethodService } from "./presenter_input_method_service"; -import { ViewerInputMethod } from "viewers/common/viewer_input_method"; +import {PresenterInputMethodService} from "./presenter_input_method_service"; +import {ViewerInputMethod} from "viewers/common/viewer_input_method"; +import {View, ViewType} from "viewers/viewer"; class ViewerInputMethodService extends ViewerInputMethod { - public getTitle(): string { - return "Input Method Service"; + override getViews(): View[] { + return [ + new View( + ViewType.TAB, + this.htmlElement, + "Input Method Service" + ) + ]; } - public getDependencies(): TraceType[] { + override getDependencies(): TraceType[] { return ViewerInputMethodService.DEPENDENCIES; } - protected initialisePresenter() { + override initialisePresenter() { return new PresenterInputMethodService(this.imeUiCallback, this.getDependencies()); } diff --git a/tools/winscope-ng/src/viewers/viewer_protolog/viewer_protolog.ts b/tools/winscope-ng/src/viewers/viewer_protolog/viewer_protolog.ts index 33f4ad112..b72d2b3ef 100644 --- a/tools/winscope-ng/src/viewers/viewer_protolog/viewer_protolog.ts +++ b/tools/winscope-ng/src/viewers/viewer_protolog/viewer_protolog.ts @@ -14,29 +14,29 @@ * limitations under the License. */ import {TraceType} from "common/trace/trace_type"; -import {Viewer} from "viewers/viewer"; +import {View, Viewer, ViewType} from "viewers/viewer"; import {Presenter} from "./presenter"; import {Events} from "./events"; import {UiData} from "./ui_data"; class ViewerProtoLog implements Viewer { constructor() { - this.view = document.createElement("viewer-protolog"); + this.htmlElement = document.createElement("viewer-protolog"); this.presenter = new Presenter((data: UiData) => { - (this.view as any).inputData = data; + (this.htmlElement as any).inputData = data; }); - this.view.addEventListener(Events.LogLevelsFilterChanged, (event) => { + this.htmlElement.addEventListener(Events.LogLevelsFilterChanged, (event) => { return this.presenter.onLogLevelsFilterChanged((event as CustomEvent).detail); }); - this.view.addEventListener(Events.TagsFilterChanged, (event) => { + this.htmlElement.addEventListener(Events.TagsFilterChanged, (event) => { return this.presenter.onTagsFilterChanged((event as CustomEvent).detail); }); - this.view.addEventListener(Events.SourceFilesFilterChanged, (event) => { + this.htmlElement.addEventListener(Events.SourceFilesFilterChanged, (event) => { return this.presenter.onSourceFilesFilterChanged((event as CustomEvent).detail); }); - this.view.addEventListener(Events.SearchStringFilterChanged, (event) => { + this.htmlElement.addEventListener(Events.SearchStringFilterChanged, (event) => { return this.presenter.onSearchStringFilterChanged((event as CustomEvent).detail); }); } @@ -45,12 +45,8 @@ class ViewerProtoLog implements Viewer { this.presenter.notifyCurrentTraceEntries(entries); } - public getView(): HTMLElement { - return this.view; - } - - public getTitle() { - return "ProtoLog"; + public getViews(): View[] { + return [new View(ViewType.TAB, this.htmlElement, "ProtoLog")]; } public getDependencies(): TraceType[] { @@ -58,7 +54,7 @@ class ViewerProtoLog implements Viewer { } public static readonly DEPENDENCIES: TraceType[] = [TraceType.PROTO_LOG]; - private view: HTMLElement; + private htmlElement: HTMLElement; private presenter: Presenter; } diff --git a/tools/winscope-ng/src/viewers/viewer_screen_recording/viewer_screen_recording.component.spec.ts b/tools/winscope-ng/src/viewers/viewer_screen_recording/viewer_screen_recording.component.spec.ts new file mode 100644 index 000000000..19c715e2c --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_screen_recording/viewer_screen_recording.component.spec.ts @@ -0,0 +1,62 @@ +/* + * 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 { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import {ComponentFixture, ComponentFixtureAutoDetect, TestBed} from "@angular/core/testing"; +import {ViewerScreenRecordingComponent} from "./viewer_screen_recording.component"; + +describe("ViewerScreenRecordingComponent", () => { + let fixture: ComponentFixture; + let component: ViewerScreenRecordingComponent; + let htmlElement: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [ + { provide: ComponentFixtureAutoDetect, useValue: true } + ], + declarations: [ + ViewerScreenRecordingComponent, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }).compileComponents(); + + fixture = TestBed.createComponent(ViewerScreenRecordingComponent); + component = fixture.componentInstance; + htmlElement = fixture.nativeElement; + + fixture.detectChanges(); + }); + + it("can be created", () => { + expect(component).toBeTruthy(); + }); + + it("can be minimized and maximized", () => { + const buttonMinimize = htmlElement.querySelector(".button-minimize"); + const video = htmlElement.querySelector("video"); + expect(buttonMinimize).toBeTruthy(); + expect(video).toBeTruthy(); + expect(video!.style.visibility).toEqual("visible"); + + buttonMinimize!.dispatchEvent(new Event("click")); + fixture.detectChanges(); + expect(video!.style.visibility).toEqual("hidden"); + + buttonMinimize!.dispatchEvent(new Event("click")); + fixture.detectChanges(); + expect(video!.style.visibility).toEqual("visible"); + }); +}); diff --git a/tools/winscope-ng/src/viewers/viewer_screen_recording/viewer_screen_recording.component.ts b/tools/winscope-ng/src/viewers/viewer_screen_recording/viewer_screen_recording.component.ts new file mode 100644 index 000000000..a306522cf --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_screen_recording/viewer_screen_recording.component.ts @@ -0,0 +1,114 @@ +/* + * 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 {Component, ElementRef, Inject, Input} from "@angular/core"; +import {DomSanitizer, SafeUrl} from "@angular/platform-browser"; +import {ScreenRecordingTraceEntry} from "common/trace/screen_recording"; + +@Component({ + selector: "viewer-screen-recording", + template: ` +
+
+ + + +
+ +
+ `, + styles: [ + ` + .container { + width: fit-content; + height: fit-content; + display: flex; + flex-direction: column; + } + + .header { + background-color: white; + border: 1px solid var(--default-border); + display: flex; + flex-direction: row; + } + + .button-drag { + flex-grow: 1; + cursor: grab; + } + + .drag-icon { + float: left; + } + + .button-minimize { + flex-grow: 0; + } + + video { + height: 50vh; + cursor: grab; + } + `, + ] +}) +class ViewerScreenRecordingComponent { + constructor( + @Inject(ElementRef) elementRef: ElementRef, + @Inject(DomSanitizer) sanitizer: DomSanitizer) { + this.elementRef = elementRef; + this.sanitizer = sanitizer; + } + + @Input() + public set currentTraceEntry(entry: undefined|ScreenRecordingTraceEntry) { + if (entry === undefined) { + return; + } + + if (this.videoUrl === undefined) { + this.videoUrl = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(entry.videoData)); + } + + this.videoCurrentTime = entry.videoTimeSeconds; + } + + public onMinimizeButtonClick() { + this.isMinimized = !this.isMinimized; + } + + public videoUrl: undefined|SafeUrl = undefined; + public videoCurrentTime = 0; + public isMinimized = false; + + private elementRef: ElementRef; + private sanitizer: DomSanitizer; +} + +export {ViewerScreenRecordingComponent}; diff --git a/tools/winscope-ng/src/viewers/viewer_screen_recording/viewer_screen_recording.ts b/tools/winscope-ng/src/viewers/viewer_screen_recording/viewer_screen_recording.ts new file mode 100644 index 000000000..38e1fe034 --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_screen_recording/viewer_screen_recording.ts @@ -0,0 +1,45 @@ +/* + * 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 {TraceType} from "common/trace/trace_type"; +import {View, Viewer, ViewType} from "viewers/viewer"; +import {ScreenRecordingTraceEntry} from "common/trace/screen_recording"; + +class ViewerScreenRecording implements Viewer { + constructor() { + this.htmlElement = document.createElement("viewer-screen-recording"); + } + + public notifyCurrentTraceEntries(entries: Map): void { + const entry: undefined | ScreenRecordingTraceEntry = entries.get(TraceType.SCREEN_RECORDING) + ? entries.get(TraceType.SCREEN_RECORDING)[0] + : undefined; + + (this.htmlElement).currentTraceEntry = entry; + } + + public getViews(): View[] { + return [new View(ViewType.OVERLAY, this.htmlElement, "ScreenRecording")]; + } + + public getDependencies(): TraceType[] { + return ViewerScreenRecording.DEPENDENCIES; + } + + public static readonly DEPENDENCIES: TraceType[] = [TraceType.SCREEN_RECORDING]; + private htmlElement: HTMLElement; +} + +export {ViewerScreenRecording}; diff --git a/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts b/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts index 7385cdfb6..69b374d62 100644 --- a/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts +++ b/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts @@ -14,40 +14,36 @@ * limitations under the License. */ import {TraceType} from "common/trace/trace_type"; -import {Viewer} from "viewers/viewer"; +import {View, Viewer, ViewType} from "viewers/viewer"; import {Presenter} from "./presenter"; import {UiData} from "./ui_data"; import {ViewerEvents} from "viewers/common/viewer_events"; class ViewerSurfaceFlinger implements Viewer { constructor() { - this.view = document.createElement("viewer-surface-flinger"); + this.htmlElement = document.createElement("viewer-surface-flinger"); this.presenter = new Presenter((uiData: UiData) => { // Angular does not deep watch @Input properties. Clearing inputData to null before repopulating // automatically ensures that the UI will change via the Angular change detection cycle. Without // resetting, Angular does not auto-detect that inputData has changed. - (this.view as any).inputData = null; - (this.view as any).inputData = uiData; + (this.htmlElement as any).inputData = null; + (this.htmlElement as any).inputData = uiData; }); - this.view.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems(((event as CustomEvent).detail.pinnedItem))); - this.view.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`)); - this.view.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions)); - this.view.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent).detail.filterString)); - this.view.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => this.presenter.updatePropertiesTree((event as CustomEvent).detail.userOptions)); - this.view.addEventListener(ViewerEvents.PropertiesFilterChange, (event) => this.presenter.filterPropertiesTree((event as CustomEvent).detail.filterString)); - this.view.addEventListener(ViewerEvents.SelectedTreeChange, (event) => this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem)); + this.htmlElement.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems(((event as CustomEvent).detail.pinnedItem))); + this.htmlElement.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`)); + this.htmlElement.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions)); + this.htmlElement.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent).detail.filterString)); + this.htmlElement.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => this.presenter.updatePropertiesTree((event as CustomEvent).detail.userOptions)); + this.htmlElement.addEventListener(ViewerEvents.PropertiesFilterChange, (event) => this.presenter.filterPropertiesTree((event as CustomEvent).detail.filterString)); + this.htmlElement.addEventListener(ViewerEvents.SelectedTreeChange, (event) => this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem)); } public notifyCurrentTraceEntries(entries: Map): void { this.presenter.notifyCurrentTraceEntries(entries); } - public getView(): HTMLElement { - return this.view; - } - - public getTitle(): string { - return "Surface Flinger"; + public getViews(): View[] { + return [new View(ViewType.TAB, this.htmlElement, "Surface Flinger")]; } public getDependencies(): TraceType[] { @@ -55,7 +51,7 @@ class ViewerSurfaceFlinger implements Viewer { } public static readonly DEPENDENCIES: TraceType[] = [TraceType.SURFACE_FLINGER]; - private view: HTMLElement; + private htmlElement: HTMLElement; private presenter: Presenter; } diff --git a/tools/winscope-ng/src/viewers/viewer_transactions/viewer_transactions.ts b/tools/winscope-ng/src/viewers/viewer_transactions/viewer_transactions.ts index 3243f0cdc..99a6fcb8b 100644 --- a/tools/winscope-ng/src/viewers/viewer_transactions/viewer_transactions.ts +++ b/tools/winscope-ng/src/viewers/viewer_transactions/viewer_transactions.ts @@ -14,36 +14,36 @@ * limitations under the License. */ import {TraceType} from "common/trace/trace_type"; -import {Viewer} from "viewers/viewer"; +import {View, Viewer, ViewType} from "viewers/viewer"; import {Presenter} from "./presenter"; import {Events} from "./events"; import {UiData} from "./ui_data"; class ViewerTransactions implements Viewer { constructor() { - this.view = document.createElement("viewer-transactions"); + this.htmlElement = document.createElement("viewer-transactions"); this.presenter = new Presenter((data: UiData) => { - (this.view as any).inputData = data; + (this.htmlElement as any).inputData = data; }); - this.view.addEventListener(Events.PidFilterChanged, (event) => { + this.htmlElement.addEventListener(Events.PidFilterChanged, (event) => { this.presenter.onPidFilterChanged((event as CustomEvent).detail); }); - this.view.addEventListener(Events.UidFilterChanged, (event) => { + this.htmlElement.addEventListener(Events.UidFilterChanged, (event) => { this.presenter.onUidFilterChanged((event as CustomEvent).detail); }); - this.view.addEventListener(Events.TypeFilterChanged, (event) => { + this.htmlElement.addEventListener(Events.TypeFilterChanged, (event) => { this.presenter.onTypeFilterChanged((event as CustomEvent).detail); }); - this.view.addEventListener(Events.IdFilterChanged, (event) => { + this.htmlElement.addEventListener(Events.IdFilterChanged, (event) => { this.presenter.onIdFilterChanged((event as CustomEvent).detail); }); - this.view.addEventListener(Events.EntryClicked, (event) => { + this.htmlElement.addEventListener(Events.EntryClicked, (event) => { this.presenter.onEntryClicked((event as CustomEvent).detail); }); } @@ -52,12 +52,8 @@ class ViewerTransactions implements Viewer { this.presenter.notifyCurrentTraceEntries(entries); } - public getView(): HTMLElement { - return this.view; - } - - public getTitle() { - return "Transactions"; + public getViews(): View[] { + return [new View(ViewType.TAB, this.htmlElement, "Transactions")]; } public getDependencies(): TraceType[] { @@ -65,7 +61,7 @@ class ViewerTransactions implements Viewer { } public static readonly DEPENDENCIES: TraceType[] = [TraceType.TRANSACTIONS]; - private view: HTMLElement; + private htmlElement: HTMLElement; private presenter: Presenter; } diff --git a/tools/winscope-ng/src/viewers/viewer_window_manager/viewer_window_manager.ts b/tools/winscope-ng/src/viewers/viewer_window_manager/viewer_window_manager.ts index 7a4eb4029..b113723a8 100644 --- a/tools/winscope-ng/src/viewers/viewer_window_manager/viewer_window_manager.ts +++ b/tools/winscope-ng/src/viewers/viewer_window_manager/viewer_window_manager.ts @@ -14,40 +14,36 @@ * limitations under the License. */ import {TraceType} from "common/trace/trace_type"; -import {Viewer} from "viewers/viewer"; +import {Viewer, View, ViewType} from "viewers/viewer"; import {Presenter} from "./presenter"; import {UiData} from "./ui_data"; import { ViewerEvents } from "viewers/common/viewer_events"; class ViewerWindowManager implements Viewer { constructor() { - this.view = document.createElement("viewer-window-manager"); + this.htmlElement = document.createElement("viewer-window-manager"); this.presenter = new Presenter((uiData: UiData) => { // Angular does not deep watch @Input properties. Clearing inputData to null before repopulating // automatically ensures that the UI will change via the Angular change detection cycle. Without // resetting, Angular does not auto-detect that inputData has changed. - (this.view as any).inputData = null; - (this.view as any).inputData = uiData; + (this.htmlElement as any).inputData = null; + (this.htmlElement as any).inputData = uiData; }); - this.view.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems(((event as CustomEvent).detail.pinnedItem))); - this.view.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`)); - this.view.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions)); - this.view.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent).detail.filterString)); - this.view.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => this.presenter.updatePropertiesTree((event as CustomEvent).detail.userOptions)); - this.view.addEventListener(ViewerEvents.PropertiesFilterChange, (event) => this.presenter.filterPropertiesTree((event as CustomEvent).detail.filterString)); - this.view.addEventListener(ViewerEvents.SelectedTreeChange, (event) => this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem)); + this.htmlElement.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems(((event as CustomEvent).detail.pinnedItem))); + this.htmlElement.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`)); + this.htmlElement.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions)); + this.htmlElement.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent).detail.filterString)); + this.htmlElement.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => this.presenter.updatePropertiesTree((event as CustomEvent).detail.userOptions)); + this.htmlElement.addEventListener(ViewerEvents.PropertiesFilterChange, (event) => this.presenter.filterPropertiesTree((event as CustomEvent).detail.filterString)); + this.htmlElement.addEventListener(ViewerEvents.SelectedTreeChange, (event) => this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem)); } public notifyCurrentTraceEntries(entries: Map): void { this.presenter.notifyCurrentTraceEntries(entries); } - public getView(): HTMLElement { - return this.view; - } - - public getTitle() { - return "Window Manager"; + public getViews(): View[] { + return [new View(ViewType.TAB, this.htmlElement, "Window Manager")]; } public getDependencies(): TraceType[] { @@ -55,7 +51,7 @@ class ViewerWindowManager implements Viewer { } public static readonly DEPENDENCIES: TraceType[] = [TraceType.WINDOW_MANAGER]; - private view: HTMLElement; + private htmlElement: HTMLElement; private presenter: Presenter; }