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;
}