diff --git a/tools/winscope-ng/package-lock.json b/tools/winscope-ng/package-lock.json index 5b94d1ca0..b06f3cb0c 100644 --- a/tools/winscope-ng/package-lock.json +++ b/tools/winscope-ng/package-lock.json @@ -23,11 +23,14 @@ "@ngrx/store": "^14.0.2", "@ngxs/store": "^3.7.4", "@types/jsbn": "^1.2.30", + "@types/three": "^0.143.0", "angular2-template-loader": "^0.6.2", "auth0": "^2.42.0", + "gl-matrix": "^3.4.3", "html-loader": "^3.1.0", "html-webpack-inline-source-plugin": "^1.0.0-beta.2", "html-webpack-plugin": "^5.5.0", + "html2canvas": "^1.4.1", "jsbn": "^1.1.0", "jsbn-rsa": "^1.0.4", "kotlin": "^1.7.0", @@ -62,7 +65,10 @@ "karma-jasmine": "~5.0.0", "karma-sourcemap-loader": "^0.3.8", "karma-webpack": "^5.0.0", - "protractor": "^7.0.0" + "protractor": "^7.0.0", + "three": "^0.143.0", + "webgl-utils": "^1.0.1", + "webgl-utils.js": "^1.1.0" } }, "node_modules/@ampproject/remapping": { @@ -3491,12 +3497,25 @@ "@types/node": "*" } }, + "node_modules/@types/three": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.143.0.tgz", + "integrity": "sha512-c5PonXOt8xk5q4ygmyjOX4Ec+FA7gwfdcMT/PveE9xrJs/0DDcf2lJkWrhEcmvx2ZefQCQBcogABnGqB0P4OsA==", + "dependencies": { + "@types/webxr": "*" + } + }, "node_modules/@types/w3c-web-usb": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.6.tgz", "integrity": "sha512-cSjhgrr8g4KbPnnijAr/KJDNKa/bBa+ixYkywFRvrhvi9n1WEl7yYbtRyzE6jqNQiSxxJxoAW3STaOQwJHndaw==", "dev": true }, + "node_modules/@types/webxr": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.0.tgz", + "integrity": "sha512-IUMDPSXnYIbEO2IereEFcgcqfDREOgmbGqtrMpVPpACTU6pltYLwHgVkrnYv0XhWEcjio9sYEfIEzgn3c7nDqA==" + }, "node_modules/@types/ws": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", @@ -4442,6 +4461,14 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -5611,6 +5638,14 @@ "postcss": "^8.4" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-loader": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz", @@ -7729,6 +7764,11 @@ "assert-plus": "^1.0.0" } }, + "node_modules/gl-matrix": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", + "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" + }, "node_modules/glob": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.1.tgz", @@ -8079,6 +8119,18 @@ "webpack": "^5.20.0" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -14399,12 +14451,26 @@ "node": "*" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/three": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.143.0.tgz", + "integrity": "sha512-oKcAGYHhJ46TGEuHjodo2n6TY2R6lbvrkp+feKZxqsUL/WkH7GKKaeu6RHeyb2Xjfk2dPLRKLsOP0KM2VgT8Zg==", + "dev": true + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -14843,6 +14909,14 @@ "node": ">= 0.4.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -15104,6 +15178,18 @@ "node": ">=0.8.0" } }, + "node_modules/webgl-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/webgl-utils/-/webgl-utils-1.0.1.tgz", + "integrity": "sha512-ox5xQ3YkrrOR6pCZHTOud49zzMXP9LnXzx7jIQvHOinV4FK59rGGJURw2Lq1cCTPgMVU2wAWq7e6vEVjz9FdBw==", + "dev": true + }, + "node_modules/webgl-utils.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/webgl-utils.js/-/webgl-utils.js-1.1.0.tgz", + "integrity": "sha512-cmO2aPd6gR6bK/ttdk8ZIypJfZMOcTvsvXv/LxXZjAFu5TC6vXqFrZYudlPuKxVsA34Pc8Fysq2rCnflu+wuuA==", + "dev": true + }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", @@ -18141,12 +18227,25 @@ "@types/node": "*" } }, + "@types/three": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.143.0.tgz", + "integrity": "sha512-c5PonXOt8xk5q4ygmyjOX4Ec+FA7gwfdcMT/PveE9xrJs/0DDcf2lJkWrhEcmvx2ZefQCQBcogABnGqB0P4OsA==", + "requires": { + "@types/webxr": "*" + } + }, "@types/w3c-web-usb": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.6.tgz", "integrity": "sha512-cSjhgrr8g4KbPnnijAr/KJDNKa/bBa+ixYkywFRvrhvi9n1WEl7yYbtRyzE6jqNQiSxxJxoAW3STaOQwJHndaw==", "dev": true }, + "@types/webxr": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.0.tgz", + "integrity": "sha512-IUMDPSXnYIbEO2IereEFcgcqfDREOgmbGqtrMpVPpACTU6pltYLwHgVkrnYv0XhWEcjio9sYEfIEzgn3c7nDqA==" + }, "@types/ws": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", @@ -18845,6 +18944,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==" + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -19749,6 +19853,14 @@ "postcss-selector-parser": "^6.0.9" } }, + "css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "requires": { + "utrie": "^1.0.2" + } + }, "css-loader": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz", @@ -21244,6 +21356,11 @@ "assert-plus": "^1.0.0" } }, + "gl-matrix": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", + "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" + }, "glob": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.1.tgz", @@ -21517,6 +21634,15 @@ "tapable": "^2.0.0" } }, + "html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "requires": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + } + }, "htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -26286,12 +26412,26 @@ } } }, + "text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "requires": { + "utrie": "^1.0.2" + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "three": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.143.0.tgz", + "integrity": "sha512-oKcAGYHhJ46TGEuHjodo2n6TY2R6lbvrkp+feKZxqsUL/WkH7GKKaeu6RHeyb2Xjfk2dPLRKLsOP0KM2VgT8Zg==", + "dev": true + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -26610,6 +26750,14 @@ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "dev": true }, + "utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "requires": { + "base64-arraybuffer": "^1.0.2" + } + }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -26816,6 +26964,18 @@ } } }, + "webgl-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/webgl-utils/-/webgl-utils-1.0.1.tgz", + "integrity": "sha512-ox5xQ3YkrrOR6pCZHTOud49zzMXP9LnXzx7jIQvHOinV4FK59rGGJURw2Lq1cCTPgMVU2wAWq7e6vEVjz9FdBw==", + "dev": true + }, + "webgl-utils.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/webgl-utils.js/-/webgl-utils.js-1.1.0.tgz", + "integrity": "sha512-cmO2aPd6gR6bK/ttdk8ZIypJfZMOcTvsvXv/LxXZjAFu5TC6vXqFrZYudlPuKxVsA34Pc8Fysq2rCnflu+wuuA==", + "dev": true + }, "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", diff --git a/tools/winscope-ng/package.json b/tools/winscope-ng/package.json index 8e0dd14af..e9b759f79 100644 --- a/tools/winscope-ng/package.json +++ b/tools/winscope-ng/package.json @@ -31,11 +31,14 @@ "@ngrx/store": "^14.0.2", "@ngxs/store": "^3.7.4", "@types/jsbn": "^1.2.30", + "@types/three": "^0.143.0", "angular2-template-loader": "^0.6.2", "auth0": "^2.42.0", + "gl-matrix": "^3.4.3", "html-loader": "^3.1.0", "html-webpack-inline-source-plugin": "^1.0.0-beta.2", "html-webpack-plugin": "^5.5.0", + "html2canvas": "^1.4.1", "jsbn": "^1.1.0", "jsbn-rsa": "^1.0.4", "kotlin": "^1.7.0", @@ -70,6 +73,9 @@ "karma-jasmine": "~5.0.0", "karma-sourcemap-loader": "^0.3.8", "karma-webpack": "^5.0.0", - "protractor": "^7.0.0" + "protractor": "^7.0.0", + "three": "^0.143.0", + "webgl-utils": "^1.0.1", + "webgl-utils.js": "^1.1.0" } } diff --git a/tools/winscope-ng/src/app/app.component.ts b/tools/winscope-ng/src/app/app.component.ts deleted file mode 100644 index d8d0fc8f5..000000000 --- a/tools/winscope-ng/src/app/app.component.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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, Inject, Injector, Input} from "@angular/core"; -import {createCustomElement} from "@angular/elements"; -import {Timestamp, TimestampType} from "common/trace/timestamp"; -import {PersistentStore} from "common/persistent_store"; -import {ViewerWindowManagerComponent} from "viewers/viewer_window_manager/viewer_window_manager.component"; -import {Core} from "./core"; -import {ProxyState, proxyClient} from "trace_collection/proxy_client"; -import { Viewer } from "viewers/viewer"; - -@Component({ - selector: "app-root", - template: ` -
- Winscope Viewer 2.0 -
- -
- - - - - - -
- -
- - Loaded data - - -
- -
- -
- -
-
- -
-
- `, - styles: [".home{width: 100%; display:flex; flex-direction: row; overflow: auto;}"] -}) -export class AppComponent { - title = "winscope-ng"; - core: Core; - states = ProxyState; - store: PersistentStore = new PersistentStore(); - @Input() dataLoaded = false; - viewersCreated = false; - - constructor( - @Inject(Injector) injector: Injector - ) { - this.core = new Core(); - if (!customElements.get("viewer-window-manager")) { - customElements.define("viewer-window-manager", - createCustomElement(ViewerWindowManagerComponent, {injector})); - } - } - - onDataLoadedChange(dataLoaded: boolean) { - if (dataLoaded && !this.viewersCreated) { - this.core.createViewers(); - this.createViewerElements(); - const dummyTimestamp = this.core.getTimestamps()[1]; //TODO: get timestamp from time scrub - this.core.notifyCurrentTimestamp(dummyTimestamp); - this.viewersCreated = true; - this.dataLoaded = dataLoaded; - } - } - - createViewerElements() { - const viewersDiv = document.querySelector("div#viewers")!; - viewersDiv.innerHTML = ""; - - this.core.getViews().forEach((view: HTMLElement) => { - viewersDiv.appendChild(view); - }); - } - - public notifyCurrentTimestamp() { - const dummyTimestamp = new Timestamp(TimestampType.ELAPSED, 1000000n); - this.core.notifyCurrentTimestamp(dummyTimestamp); - } - - public clearData() { - this.dataLoaded = false; - this.viewersCreated = false; - this.core.clearData(); - proxyClient.adbData = []; - } -} diff --git a/tools/winscope-ng/src/app/app.module.ts b/tools/winscope-ng/src/app/app.module.ts index 659bd16ba..92c3da9d8 100644 --- a/tools/winscope-ng/src/app/app.module.ts +++ b/tools/winscope-ng/src/app/app.module.ts @@ -13,27 +13,40 @@ import { MatFormFieldModule } from "@angular/material/form-field"; import { MatIconModule } from "@angular/material/icon"; import { MatInputModule } from "@angular/material/input"; import { MatSelectModule } from "@angular/material/select"; +import { MatRadioModule } from "@angular/material/radio"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { HttpClientModule } from "@angular/common/http"; +import { MatSliderModule } from "@angular/material/slider"; -import { AppComponent } from "./app.component"; +import { AppComponent } from "./components/app.component"; import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/viewer_window_manager.component"; -import { CollectTracesComponent } from "./collect_traces.component"; -import { AdbProxyComponent } from "./adb_proxy.component"; -import { WebAdbComponent } from "./web_adb.component"; -import { TraceConfigComponent } from "./trace_config.component"; -import { UploadTracesComponent } from "./upload_traces.component"; - +import { ViewerSurfaceFlingerComponent } from "viewers/viewer_surface_flinger/viewer_surface_flinger.component"; +import { CollectTracesComponent } from "./components/collect_traces.component"; +import { AdbProxyComponent } from "./components/adb_proxy.component"; +import { WebAdbComponent } from "./components/web_adb.component"; +import { TraceConfigComponent } from "./components/trace_config.component"; +import { UploadTracesComponent } from "./components/upload_traces.component"; +import { HierarchyComponent } from "viewers/hierarchy.component"; +import { PropertiesComponent } from "viewers/properties.component"; +import { RectsComponent } from "viewers/rects.component"; +import { TraceViewHeaderComponent } from "./components/trace_view_header.component"; +import { TraceViewComponent } from "./components/trace_view.component"; @NgModule({ declarations: [ AppComponent, ViewerWindowManagerComponent, + ViewerSurfaceFlingerComponent, CollectTracesComponent, UploadTracesComponent, AdbProxyComponent, WebAdbComponent, TraceConfigComponent, + HierarchyComponent, + PropertiesComponent, + RectsComponent, + TraceViewHeaderComponent, + TraceViewComponent ], imports: [ BrowserModule, @@ -52,9 +65,10 @@ import { UploadTracesComponent } from "./upload_traces.component"; MatInputModule, MatSelectModule, BrowserAnimationsModule, - HttpClientModule + HttpClientModule, + MatSliderModule, + MatRadioModule ], - providers: [], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/tools/winscope-ng/src/app/adb_proxy.component.spec.ts b/tools/winscope-ng/src/app/components/adb_proxy.component.spec.ts similarity index 97% rename from tools/winscope-ng/src/app/adb_proxy.component.spec.ts rename to tools/winscope-ng/src/app/components/adb_proxy.component.spec.ts index f048b6b55..1e7187aa6 100644 --- a/tools/winscope-ng/src/app/adb_proxy.component.spec.ts +++ b/tools/winscope-ng/src/app/components/adb_proxy.component.spec.ts @@ -16,7 +16,7 @@ import { CommonModule } from "@angular/common"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { AdbProxyComponent } from "./adb_proxy.component"; -import { proxyClient, ProxyState } from "../trace_collection/proxy_client"; +import { proxyClient, ProxyState } from "trace_collection/proxy_client"; import { MatIconModule } from "@angular/material/icon"; import { MatFormFieldModule } from "@angular/material/form-field"; import { MatInputModule } from "@angular/material/input"; diff --git a/tools/winscope-ng/src/app/adb_proxy.component.ts b/tools/winscope-ng/src/app/components/adb_proxy.component.ts similarity index 97% rename from tools/winscope-ng/src/app/adb_proxy.component.ts rename to tools/winscope-ng/src/app/components/adb_proxy.component.ts index 81f6c79dd..2f80b5766 100644 --- a/tools/winscope-ng/src/app/adb_proxy.component.ts +++ b/tools/winscope-ng/src/app/components/adb_proxy.component.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { Component, Input, Output, EventEmitter } from "@angular/core"; -import { proxyClient, ProxyClient, ProxyState } from "../trace_collection/proxy_client"; +import { proxyClient, ProxyClient, ProxyState } from "trace_collection/proxy_client"; @Component({ selector: "adb-proxy", diff --git a/tools/winscope-ng/src/app/app.component.spec.ts b/tools/winscope-ng/src/app/components/app.component.spec.ts similarity index 88% rename from tools/winscope-ng/src/app/app.component.spec.ts rename to tools/winscope-ng/src/app/components/app.component.spec.ts index a8e00c87d..c1526d03b 100644 --- a/tools/winscope-ng/src/app/app.component.spec.ts +++ b/tools/winscope-ng/src/app/components/app.component.spec.ts @@ -28,6 +28,8 @@ import { WebAdbComponent } from "./web_adb.component"; import { TraceConfigComponent } from "./trace_config.component"; import { ComponentFixtureAutoDetect } from "@angular/core/testing"; +import { ViewerSurfaceFlingerComponent } from "viewers/viewer_surface_flinger/viewer_surface_flinger.component"; +import { MatSliderModule } from "@angular/material/slider"; describe("AppComponent", () => { @@ -45,6 +47,7 @@ describe("AppComponent", () => { MatCardModule, MatButtonModule, MatGridListModule, + MatSliderModule ], declarations: [ AppComponent, @@ -53,6 +56,7 @@ describe("AppComponent", () => { AdbProxyComponent, WebAdbComponent, TraceConfigComponent, + ViewerSurfaceFlingerComponent ], }).overrideComponent(AppComponent, { set: { changeDetection: ChangeDetectionStrategy.Default } @@ -71,7 +75,7 @@ describe("AppComponent", () => { }); it("renders the page title", () => { - expect(htmlElement.querySelector("#title")?.innerHTML).toContain("Winscope Viewer 2.0"); + expect(htmlElement.querySelector("#app-title")?.innerHTML).toContain("Winscope Viewer 2.0"); }); it("displays correct elements when no data loaded", async () => { @@ -87,6 +91,6 @@ describe("AppComponent", () => { fixture.detectChanges(); expect(htmlElement.querySelector("#collect-traces-card")).toBeFalsy(); expect(htmlElement.querySelector("#upload-traces-card")).toBeFalsy(); - expect(htmlElement.querySelector("#loaded-data-card")).toBeTruthy(); + expect(htmlElement.querySelector(".viewers.show")).toBeTruthy(); }); }); diff --git a/tools/winscope-ng/src/app/components/app.component.ts b/tools/winscope-ng/src/app/components/app.component.ts new file mode 100644 index 000000000..b854e370f --- /dev/null +++ b/tools/winscope-ng/src/app/components/app.component.ts @@ -0,0 +1,168 @@ +/* + * 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, Injector, Inject, ViewEncapsulation, Input } from "@angular/core"; +import { createCustomElement } from "@angular/elements"; +import { TraceCoordinator } from "../trace_coordinator"; +import { proxyClient, ProxyState } from "trace_collection/proxy_client"; +import { PersistentStore } from "common/persistent_store"; +import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/viewer_window_manager.component"; +import { ViewerSurfaceFlingerComponent } from "viewers/viewer_surface_flinger/viewer_surface_flinger.component"; +import { TraceViewComponent } from "./trace_view.component"; +import { Timestamp } from "common/trace/timestamp"; +import { MatSliderChange } from "@angular/material/slider"; +import { Viewer } from "viewers/viewer"; + +@Component({ + selector: "app-root", + template: ` +
+ Winscope Viewer 2.0 + + + +
+ +
+ + + + + + +
+ +
+
+ +
+
+ +
+
+ `, + styles: [".time-slider {width: 100%}"], + encapsulation: ViewEncapsulation.None +}) +export class AppComponent { + title = "winscope-ng"; + traceCoordinator: TraceCoordinator; + states = ProxyState; + store: PersistentStore = new PersistentStore(); + @Input() dataLoaded = false; + viewersCreated = false; + currentTimestamp?: Timestamp; + currentTimestampIndex = 0; + allTimestamps: Timestamp[] = []; + + constructor( + @Inject(Injector) injector: Injector + ) { + this.traceCoordinator = new TraceCoordinator(); + if (!customElements.get("viewer-window-manager")) { + customElements.define("viewer-window-manager", + createCustomElement(ViewerWindowManagerComponent, {injector})); + } + if (!customElements.get("viewer-surface-flinger")) { + customElements.define("viewer-surface-flinger", + createCustomElement(ViewerSurfaceFlingerComponent, {injector})); + } + if (!customElements.get("trace-view")) { + customElements.define("trace-view", + createCustomElement(TraceViewComponent, {injector})); + } + } + + onDataLoadedChange(dataLoaded: boolean) { + if (dataLoaded && !this.viewersCreated) { + this.allTimestamps = this.traceCoordinator.getTimestamps(); + this.traceCoordinator.createViewers(); + this.createViewerElements(); + this.currentTimestampIndex = 0; + this.notifyCurrentTimestamp(); + this.viewersCreated = true; + this.dataLoaded = dataLoaded; + } + } + + createViewerElements() { + const viewersDiv = document.querySelector("div#viewers")!; + viewersDiv.innerHTML = ""; + + let cardCounter = 0; + this.traceCoordinator.getViewers().forEach((viewer: Viewer) => { + const traceView = document.createElement("trace-view"); + (traceView as any).title = viewer.getTitle(); + (traceView as any).dependencies = viewer.getDependencies(); + (traceView as any).showTrace = true; + traceView.addEventListener("saveTraces", ($event: any) => { + this.traceCoordinator.saveTraces($event.detail); + }); + viewersDiv.appendChild(traceView); + + const traceCard = traceView.querySelector(".trace-card")!; + traceCard.id = `card-${cardCounter}`; + (traceView as any).cardId = cardCounter; + cardCounter++; + + const traceCardContent = traceCard.querySelector(".trace-card-content")!; + const view = viewer.getView(); + (view as any).showTrace = (traceView as any).showTrace; + traceCardContent.appendChild(view); + }); + } + + updateCurrentTimestamp(event: MatSliderChange) { + if (event.value) { + this.currentTimestampIndex = event.value; + this.notifyCurrentTimestamp(); + } + } + + public notifyCurrentTimestamp() { + this.currentTimestamp = this.allTimestamps[this.currentTimestampIndex]; + this.traceCoordinator.notifyCurrentTimestamp(this.currentTimestamp); + } + + public toggleTimestamp() { + if (this.currentTimestampIndex===0) { + this.currentTimestampIndex = this.allTimestamps.length-1; + } else { + this.currentTimestampIndex = 0; + } + this.notifyCurrentTimestamp(); + } + + public clearData() { + this.dataLoaded = false; + this.viewersCreated = false; + this.traceCoordinator.clearData(); + proxyClient.adbData = []; + } + + public showViewers() { + const isShown = this.dataLoaded ? "show" : "hide"; + return ["viewers", isShown]; + } +} diff --git a/tools/winscope-ng/src/app/collect_traces.component.spec.ts b/tools/winscope-ng/src/app/components/collect_traces.component.spec.ts similarity index 99% rename from tools/winscope-ng/src/app/collect_traces.component.spec.ts rename to tools/winscope-ng/src/app/components/collect_traces.component.spec.ts index 7f4189c32..b021204dd 100644 --- a/tools/winscope-ng/src/app/collect_traces.component.spec.ts +++ b/tools/winscope-ng/src/app/components/collect_traces.component.spec.ts @@ -24,6 +24,7 @@ import { MatListModule } from "@angular/material/list"; import { MatButtonModule } from "@angular/material/button"; import { MatProgressBarModule } from "@angular/material/progress-bar"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { NO_ERRORS_SCHEMA } from "@angular/core"; describe("CollectTracesComponent", () => { let fixture: ComponentFixture; @@ -36,7 +37,6 @@ describe("CollectTracesComponent", () => { MatIconModule, MatCardModule, MatListModule, - MatIconModule, MatButtonModule, MatProgressBarModule, BrowserAnimationsModule @@ -47,6 +47,7 @@ describe("CollectTracesComponent", () => { WebAdbComponent, TraceConfigComponent, ], + schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); fixture = TestBed.createComponent(CollectTracesComponent); component = fixture.componentInstance; diff --git a/tools/winscope-ng/src/app/collect_traces.component.ts b/tools/winscope-ng/src/app/components/collect_traces.component.ts similarity index 87% rename from tools/winscope-ng/src/app/collect_traces.component.ts rename to tools/winscope-ng/src/app/components/collect_traces.component.ts index aaffc66bf..c9939bb4f 100644 --- a/tools/winscope-ng/src/app/collect_traces.component.ts +++ b/tools/winscope-ng/src/app/components/collect_traces.component.ts @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component, Input, OnInit, Output, EventEmitter, NgZone, Inject } from "@angular/core"; +import { Component, Inject, Input, Output, EventEmitter, OnInit, OnDestroy } from "@angular/core"; import { ProxyConnection } from "trace_collection/proxy_connection"; import { Connection } from "trace_collection/connection"; import { setTraces } from "trace_collection/set_traces"; -import { ProxyState } from "../trace_collection/proxy_client"; -import { traceConfigurations, configMap, SelectionConfiguration, EnableConfiguration } from "../trace_collection/trace_collection_utils"; -import { Core } from "app/core"; -import { PersistentStore } from "../common/persistent_store"; +import { ProxyState } from "trace_collection/proxy_client"; +import { traceConfigurations, configMap, SelectionConfiguration, EnableConfiguration } from "trace_collection/trace_collection_utils"; +import { TraceCoordinator } from "app/trace_coordinator"; +import { PersistentStore } from "common/persistent_store"; @Component({ @@ -33,9 +33,9 @@ import { PersistentStore } from "../common/persistent_store";
- + - +
@@ -110,30 +110,26 @@ import { PersistentStore } from "../common/persistent_store"; `, - styles: [".device-choice {cursor: pointer}"] + styles: [ + ".device-choice {cursor: pointer}", + ".mat-checkbox .mat-checkbox-frame {transform: scale(0.7); font-size: 10;}", + ".mat-checkbox-checked .mat-checkbox-background {transform: scale(0.7); font-size: 10;}" + ] }) -export class CollectTracesComponent implements OnInit { +export class CollectTracesComponent implements OnInit, OnDestroy { objectKeys = Object.keys; isAdbProxy = true; traceConfigurations = traceConfigurations; connect: Connection = new ProxyConnection(); setTraces = setTraces; - - @Input() - store: PersistentStore = new PersistentStore(); - - @Input() - core?: Core; - - @Output() - coreChange = new EventEmitter(); - dataLoaded = false; - @Output() - dataLoadedChange = new EventEmitter(); + @Input() store!: PersistentStore; + @Input() traceCoordinator!: TraceCoordinator; - ngOnInit(): void { + @Output() dataLoadedChange = new EventEmitter(); + + ngOnInit() { if (this.isAdbProxy) { this.connect = new ProxyConnection(); } else { @@ -142,8 +138,6 @@ export class CollectTracesComponent implements OnInit { } } - constructor(@Inject(NgZone) private ngZone: NgZone) {} - ngOnDestroy(): void { this.connect.proxy?.removeOnProxyChange(this.onProxyChange); } @@ -258,7 +252,7 @@ export class CollectTracesComponent implements OnInit { if (!setTraces.dumpError) { await this.loadFiles(); } else { - this.core?.clearData(); + this.traceCoordinator.clearData(); } } @@ -273,13 +267,12 @@ export class CollectTracesComponent implements OnInit { public async loadFiles() { console.log("loading files", this.connect.adbData()); - this.core?.clearData(); - await this.core?.addTraces(this.connect.adbData()); - this.ngZone.run(() => { - this.dataLoaded = true; - this.dataLoadedChange.emit(this.dataLoaded); - console.log("finished loading data!"); - }); + this.traceCoordinator.clearData(); + + await this.traceCoordinator.addTraces(this.connect.adbData()); + this.dataLoaded = true; + this.dataLoadedChange.emit(this.dataLoaded); + console.log("finished loading data!"); } public tabClass(adbTab: boolean) { diff --git a/tools/winscope-ng/src/app/trace_config.component.spec.ts b/tools/winscope-ng/src/app/components/trace_config.component.spec.ts similarity index 98% rename from tools/winscope-ng/src/app/trace_config.component.spec.ts rename to tools/winscope-ng/src/app/components/trace_config.component.spec.ts index 8b2cec2b4..2a53befcb 100644 --- a/tools/winscope-ng/src/app/trace_config.component.spec.ts +++ b/tools/winscope-ng/src/app/components/trace_config.component.spec.ts @@ -104,7 +104,7 @@ describe("TraceConfigComponent", () => { expect(adv?.innerHTML).toContain("tracing level"); }); - it("check that changing enable config causes box to change", async () => { spyOn(component, "changeTraceCollectionConfig"); + it("check that changing enable config causes box to change", async () => { component.trace.config!.enableConfigs[0].enabled = false; fixture.detectChanges(); await fixture.whenStable(); diff --git a/tools/winscope-ng/src/app/trace_config.component.ts b/tools/winscope-ng/src/app/components/trace_config.component.ts similarity index 98% rename from tools/winscope-ng/src/app/trace_config.component.ts rename to tools/winscope-ng/src/app/components/trace_config.component.ts index b329781d2..357ba5f97 100644 --- a/tools/winscope-ng/src/app/trace_config.component.ts +++ b/tools/winscope-ng/src/app/components/trace_config.component.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { Component, Input } from "@angular/core"; -import { EnableConfiguration, SelectionConfiguration, TraceConfiguration } from "../trace_collection/trace_collection_utils"; +import { EnableConfiguration, SelectionConfiguration, TraceConfiguration } from "trace_collection/trace_collection_utils"; @Component({ selector: "trace-config", 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 new file mode 100644 index 000000000..c589351eb --- /dev/null +++ b/tools/winscope-ng/src/app/components/trace_view.component.spec.ts @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { CommonModule } from "@angular/common"; +import { 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 { TraceType } from "common/trace/trace_type"; + +describe("TraceViewComponent", () => { + let fixture: ComponentFixture; + let component: TraceViewComponent; + let htmlElement: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + CommonModule, + MatCardModule + ], + declarations: [TraceViewComponent], + schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA] + }).compileComponents(); + fixture = TestBed.createComponent(TraceViewComponent); + component = fixture.componentInstance; + htmlElement = fixture.nativeElement; + component.dependencies = [TraceType.SURFACE_FLINGER, TraceType.WINDOW_MANAGER]; + component.showTrace = true; + }); + + it("can be created", () => { + expect(component).toBeTruthy(); + }); + + it("check that mat card title and contents are displayed", () => { + fixture.detectChanges(); + const title = htmlElement.querySelector(".trace-card-title"); + expect(title).toBeTruthy(); + const header = title?.querySelector("trace-view-header"); + expect(header).toBeTruthy(); + }); + + it("check that card content is created", () => { + fixture.detectChanges(); + const content = htmlElement.querySelector(".trace-card-content") as HTMLElement; + expect(content).toBeTruthy(); + }); +}); diff --git a/tools/winscope-ng/src/app/components/trace_view.component.ts b/tools/winscope-ng/src/app/components/trace_view.component.ts new file mode 100644 index 000000000..cfb56e594 --- /dev/null +++ b/tools/winscope-ng/src/app/components/trace_view.component.ts @@ -0,0 +1,57 @@ +/* + * 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, + Input, + Output, + EventEmitter +} from "@angular/core"; +import { TRACE_INFO } from "../trace_info"; +import { TraceType } from "common/trace/trace_type"; + +@Component({ + selector: "trace-view", + template: ` + + + + + + + + + + `, +}) +export class TraceViewComponent { + @Input() title!: string; + @Input() dependencies!: TraceType[]; + @Input() showTrace = true; + @Input() cardId = 0; + @Output() saveTraces = new EventEmitter(); + + TRACE_INFO = TRACE_INFO; + + onSaveTraces(dependencies: TraceType[]) { + this.saveTraces.emit(dependencies); + } +} diff --git a/tools/winscope-ng/src/app/components/trace_view_header.component.spec.ts b/tools/winscope-ng/src/app/components/trace_view_header.component.spec.ts new file mode 100644 index 000000000..6a80064a1 --- /dev/null +++ b/tools/winscope-ng/src/app/components/trace_view_header.component.spec.ts @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { CommonModule } from "@angular/common"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { TraceViewHeaderComponent } from "./trace_view_header.component"; +import { MatIconModule } from "@angular/material/icon"; +import { MatButtonModule } from "@angular/material/button"; +import { TraceType } from "common/trace/trace_type"; + +describe("TraceViewHeaderComponent", () => { + let fixture: ComponentFixture; + let component: TraceViewHeaderComponent; + let htmlElement: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + CommonModule, + MatIconModule, + MatButtonModule + ], + declarations: [TraceViewHeaderComponent] + }).compileComponents(); + fixture = TestBed.createComponent(TraceViewHeaderComponent); + component = fixture.componentInstance; + htmlElement = fixture.nativeElement; + component.dependencies = [TraceType.SURFACE_FLINGER, TraceType.WINDOW_MANAGER]; + }); + + it("can be created", () => { + expect(component).toBeTruthy(); + }); + + it("check that toggle button is displayed, expanded on default", () => { + component.showTrace = true; + fixture.detectChanges(); + const toggleButton = htmlElement.querySelector("#toggle-btn"); + expect(toggleButton).toBeTruthy(); + const chevronIcon = toggleButton?.querySelector("mat-icon"); + expect(chevronIcon).toBeTruthy; + expect(chevronIcon?.innerHTML).toContain("expand_more"); + }); + + it("check that toggle button icon is a right chevron when minimised ", () => { + component.showTrace = false; + fixture.detectChanges(); + const toggleButton = htmlElement.querySelector("#toggle-btn"); + const chevronIcon = toggleButton?.querySelector("mat-icon"); + expect(chevronIcon?.innerHTML).toContain("chevron_right"); + }); + + it("check that clicking toggle button causes view to minimise", async () => { + component.showTrace = true; + fixture.detectChanges(); + spyOn(component, "toggleView").and.callThrough(); + const button: HTMLButtonElement | null = htmlElement.querySelector("#toggle-btn"); + expect(button).toBeInstanceOf(HTMLButtonElement); + button?.dispatchEvent(new Event("click")); + await fixture.whenStable(); + expect(component.toggleView).toHaveBeenCalled(); + fixture.detectChanges(); + expect (htmlElement.querySelector("#toggle-btn")?.querySelector("mat-icon")?.innerHTML).toContain("chevron_right"); + }); + + it("check that dependency icons show", () => { + fixture.detectChanges(); + const dependencyIcons = htmlElement.querySelectorAll("#dep-icon"); + expect(dependencyIcons).toBeTruthy(); + expect(dependencyIcons.length).toBe(2); + }); + + it("check that title is displayed", () => { + component.title = "Surface Flinger, Window Manager"; + fixture.detectChanges(); + const title = htmlElement.querySelector(".trace-card-title-text"); + expect(title).toBeTruthy(); + expect(title?.innerHTML).toContain("Surface Flinger"); + expect(title?.innerHTML).toContain("Window Manager"); + }); + + it("check that save button is displayed", () => { + fixture.detectChanges(); + const saveButton = htmlElement.querySelectorAll("#save-btn"); + expect(saveButton).toBeTruthy(); + }); + + it("check that clicking save button emits", async () => { + spyOn(component, "saveTraces").and.callThrough(); + spyOn(component.saveTraceChange, "emit"); + const button: HTMLButtonElement | null = htmlElement.querySelector("#save-btn"); + expect(button).toBeInstanceOf(HTMLButtonElement); + button?.dispatchEvent(new Event("click")); + await fixture.whenStable(); + expect(component.saveTraces).toHaveBeenCalled(); + expect(component.saveTraceChange.emit).toHaveBeenCalled(); + }); + + it("check that screenshot button is displayed", () => { + fixture.detectChanges(); + const screenshotButton = htmlElement.querySelectorAll("#screenshot-btn"); + expect(screenshotButton).toBeTruthy(); + }); +}); diff --git a/tools/winscope-ng/src/app/components/trace_view_header.component.ts b/tools/winscope-ng/src/app/components/trace_view_header.component.ts new file mode 100644 index 000000000..a298d8507 --- /dev/null +++ b/tools/winscope-ng/src/app/components/trace_view_header.component.ts @@ -0,0 +1,88 @@ +/* + * 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, + Input, + Output, + EventEmitter +} from "@angular/core"; +import { TRACE_INFO } from "../trace_info"; +import { TraceType } from "common/trace/trace_type"; +import html2canvas from "html2canvas"; + +@Component({ + selector: "trace-view-header", + template: ` + + + + {{title}} + + + + `, + styles: [ + ".trace-card-title {font: inherit; display: inline-block; vertical-align: middle;}", + ] +}) +export class TraceViewHeaderComponent { + @Input() title?: string; + @Input() dependencies?: TraceType[]; + @Input() showTrace = true; + @Input() cardId!: number ; + + @Output() showTraceChange = new EventEmitter(); + @Output() saveTraceChange = new EventEmitter(); + + TRACE_INFO = TRACE_INFO; + + toggleView() { + this.showTrace = !this.showTrace; + this.showTraceChange.emit(this.showTrace); + } + + public saveTraces() { + this.saveTraceChange.emit(this.dependencies); + } + + public takeScreenshot() { + const el = document.querySelector(`#card-${this.cardId}`); + if (el) { + html2canvas((el as HTMLElement)).then((canvas) => { + const uri = canvas.toDataURL(); + const filename = "Winscope-Screenshot.png"; + const link = document.createElement("a"); + if (typeof link.download === "string") { + link.href = uri; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } else { + window.open(uri); + } + }); + } + } +} diff --git a/tools/winscope-ng/src/app/upload_traces.component.spec.ts b/tools/winscope-ng/src/app/components/upload_traces.component.spec.ts similarity index 96% rename from tools/winscope-ng/src/app/upload_traces.component.spec.ts rename to tools/winscope-ng/src/app/components/upload_traces.component.spec.ts index d617dbd38..6322423eb 100644 --- a/tools/winscope-ng/src/app/upload_traces.component.spec.ts +++ b/tools/winscope-ng/src/app/components/upload_traces.component.spec.ts @@ -17,7 +17,7 @@ import {ComponentFixture, TestBed} from "@angular/core/testing"; import {UploadTracesComponent} from "./upload_traces.component"; import { MatCardModule } from "@angular/material/card"; -describe("CollectTracesComponent", () => { +describe("UploadTracesComponent", () => { let fixture: ComponentFixture; let component: UploadTracesComponent; let htmlElement: HTMLElement; diff --git a/tools/winscope-ng/src/app/upload_traces.component.ts b/tools/winscope-ng/src/app/components/upload_traces.component.ts similarity index 81% rename from tools/winscope-ng/src/app/upload_traces.component.ts rename to tools/winscope-ng/src/app/components/upload_traces.component.ts index 7e69f1f81..b9a7ca720 100644 --- a/tools/winscope-ng/src/app/upload_traces.component.ts +++ b/tools/winscope-ng/src/app/components/upload_traces.component.ts @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component, Input, Inject, Output, EventEmitter, NgZone } from "@angular/core"; -import { Core } from "app/core"; -import { TRACE_ICONS } from "app/trace_icons"; +import { Component, Input, Output, EventEmitter, Inject, NgZone } from "@angular/core"; +import { TraceCoordinator } from "app/trace_coordinator"; +import { TRACE_INFO } from "app/trace_info"; import { LoadedTrace } from "app/loaded_trace"; @Component({ @@ -57,45 +57,43 @@ import { LoadedTrace } from "app/loaded_trace"; *ngIf="this.loadedTraces.length > 0" > - {{TRACE_ICONS[trace.type]}} + {{TRACE_INFO[trace.type].icon}} {{trace.name}} ({{trace.type}}) `, - styles: [".drop-info{font-weight: normal;}"] + styles: [ + ".drop-info{font-weight: normal; pointer-events: none;}", + ] }) export class UploadTracesComponent { - @Input() - core?: Core; - - @Output() - coreChange = new EventEmitter(); + @Input() traceCoordinator!: TraceCoordinator; dataLoaded = false; - @Output() - dataLoadedChange = new EventEmitter(); - - loadedTraces: LoadedTrace[] = []; - TRACE_ICONS = TRACE_ICONS; + @Output() dataLoadedChange = new EventEmitter(); constructor(@Inject(NgZone) private ngZone: NgZone) {} + loadedTraces: LoadedTrace[] = []; + TRACE_INFO = TRACE_INFO; + public async onInputFile(event: Event) { const files = this.getInputFiles(event); await this.processFiles(files); } public async processFiles(files: File[]) { - await this.core?.addTraces(files); + await this.traceCoordinator.addTraces(files); this.ngZone.run(() => { - if (this.core) this.loadedTraces = this.core.getLoadedTraces(); + this.loadedTraces = this.traceCoordinator.getLoadedTraces(); }); } @@ -114,7 +112,10 @@ export class UploadTracesComponent { } public onClearData() { - this.core?.clearData(); + this.traceCoordinator.clearData(); + this.dataLoaded = false; + this.loadedTraces = []; + this.dataLoadedChange.emit(this.dataLoaded); } public onFileDragIn(e: DragEvent) { @@ -136,7 +137,7 @@ export class UploadTracesComponent { } public onRemoveTrace(trace: LoadedTrace) { - this.core?.removeTrace(trace.type); + this.traceCoordinator.removeTrace(trace.type); this.loadedTraces = this.loadedTraces.filter(loaded => loaded.type !== trace.type); } } diff --git a/tools/winscope-ng/src/app/web_adb.component.spec.ts b/tools/winscope-ng/src/app/components/web_adb.component.spec.ts similarity index 100% rename from tools/winscope-ng/src/app/web_adb.component.spec.ts rename to tools/winscope-ng/src/app/components/web_adb.component.spec.ts diff --git a/tools/winscope-ng/src/app/web_adb.component.ts b/tools/winscope-ng/src/app/components/web_adb.component.ts similarity index 100% rename from tools/winscope-ng/src/app/web_adb.component.ts rename to tools/winscope-ng/src/app/components/web_adb.component.ts diff --git a/tools/winscope-ng/src/app/loaded_trace.ts b/tools/winscope-ng/src/app/loaded_trace.ts index 137b56727..c24cf649c 100644 --- a/tools/winscope-ng/src/app/loaded_trace.ts +++ b/tools/winscope-ng/src/app/loaded_trace.ts @@ -1,4 +1,4 @@ -import { TraceType } from "../common/trace/trace_type"; +import { TraceType } from "common/trace/trace_type"; export interface LoadedTrace { name: string; diff --git a/tools/winscope-ng/src/app/core.ts b/tools/winscope-ng/src/app/trace_coordinator.ts similarity index 75% rename from tools/winscope-ng/src/app/core.ts rename to tools/winscope-ng/src/app/trace_coordinator.ts index 30ce5b927..bd889bee5 100644 --- a/tools/winscope-ng/src/app/core.ts +++ b/tools/winscope-ng/src/app/trace_coordinator.ts @@ -21,8 +21,9 @@ import { setTraces } from "trace_collection/set_traces"; import { Viewer } from "viewers/viewer"; import { ViewerFactory } from "viewers/viewer_factory"; import { LoadedTrace } from "app/loaded_trace"; +import { TRACE_INFO } from "./trace_info"; -class Core { +class TraceCoordinator { private parsers: Parser[]; private viewers: Viewer[]; @@ -37,7 +38,6 @@ class Core { console.log("created parsers: ", this.parsers); } - removeTrace(type: TraceType) { this.parsers = this.parsers.filter(parser => parser.getTraceType() !== type); } @@ -62,10 +62,19 @@ class Core { return this.viewers.map(viewer => viewer.getView()); } + getViewers(): Viewer[] { + return this.viewers; + } + loadedTraceTypes(): TraceType[] { return this.parsers.map(parser => parser.getTraceType()); } + findParser(fileType: TraceType): Parser | null { + const parser = this.parsers.find(parser => parser.getTraceType() === fileType); + return parser ?? null; + } + getTimestamps(): Timestamp[] { for (const type of [TimestampType.REAL, TimestampType.ELAPSED]) { const mergedTimestamps: Timestamp[] = []; @@ -94,8 +103,9 @@ class Core { const traceEntries: Map = new Map(); this.parsers.forEach(parser => { - const entry = parser.getTraceEntry(timestamp); - if (entry != undefined) { + const targetTimestamp = timestamp; + const entry = parser.getTraceEntry(targetTimestamp); + if (entry !== undefined) { traceEntries.set(parser.getTraceType(), entry); } }); @@ -106,10 +116,31 @@ class Core { } clearData() { + this.getViews().forEach(view => view.remove()); this.parsers = []; this.viewers = []; setTraces.dataReady = false; } + + saveTraces(traceTypes: TraceType[]) { + const blobs: Blob[] = []; + traceTypes.forEach(type => { + const trace = this.findParser(type)?.getTrace(); + if (trace) { + blobs.push(trace); + } + }); + blobs.forEach((blob, idx) => { + const a = document.createElement("a"); + document.body.appendChild(a); + const url = window.URL.createObjectURL(blob); + a.href = url; + a.download = (blob as any).name ?? `${TRACE_INFO[traceTypes[idx]].name}.pb`; + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + }); + } } -export { Core }; \ No newline at end of file +export { TraceCoordinator }; \ No newline at end of file diff --git a/tools/winscope-ng/src/app/trace_icons.ts b/tools/winscope-ng/src/app/trace_icons.ts index 14cec84d7..104c0ff32 100644 --- a/tools/winscope-ng/src/app/trace_icons.ts +++ b/tools/winscope-ng/src/app/trace_icons.ts @@ -1,4 +1,4 @@ -import { TraceType } from "../common/trace/trace_type"; +import { TraceType } from "common/trace/trace_type"; const WINDOW_MANAGER_ICON = "view_compact"; const SURFACE_FLINGER_ICON = "filter_none"; diff --git a/tools/winscope-ng/src/app/trace_info.ts b/tools/winscope-ng/src/app/trace_info.ts new file mode 100644 index 000000000..99999d5b6 --- /dev/null +++ b/tools/winscope-ng/src/app/trace_info.ts @@ -0,0 +1,88 @@ +import { TraceType } from "common/trace/trace_type"; + +const WINDOW_MANAGER_ICON = "view_compact"; +const SURFACE_FLINGER_ICON = "filter_none"; +const SCREEN_RECORDING_ICON = "videocam"; +const TRANSACTION_ICON = "timeline"; +const WAYLAND_ICON = "filter_none"; +const PROTO_LOG_ICON = "notes"; +const SYSTEM_UI_ICON = "filter_none"; +const LAUNCHER_ICON = "filter_none"; +const IME_ICON = "keyboard"; +const ACCESSIBILITY_ICON = "accessibility"; +const TAG_ICON = "details"; +const TRACE_ERROR_ICON = "warning"; + +type traceInfoMap = { + [key: number]: { + name: string, + icon: string + }; +} + +export const TRACE_INFO: traceInfoMap = { + [TraceType.ACCESSIBILITY]: { + name: "Accessibility", + icon: ACCESSIBILITY_ICON + }, + [TraceType.WINDOW_MANAGER]: { + name: "Window Manager", + icon: WINDOW_MANAGER_ICON + }, + [TraceType.SURFACE_FLINGER]: { + name: "Surface Flinger", + icon: SURFACE_FLINGER_ICON + }, + [TraceType.SCREEN_RECORDING]: { + name: "Screen Recording", + icon: SCREEN_RECORDING_ICON + }, + [TraceType.TRANSACTIONS]: { + name: "Transactions", + icon: TRANSACTION_ICON + }, + [TraceType.TRANSACTIONS_LEGACY]: { + name: "Transactions Legacy", + icon: TRANSACTION_ICON + }, + [TraceType.WAYLAND]: { + name: "Wayland", + icon: WAYLAND_ICON + }, + [TraceType.WAYLAND_DUMP]: { + name: "Wayland Dump", + icon: WAYLAND_ICON + }, + [TraceType.PROTO_LOG]: { + name: "Proto Log", + icon: PROTO_LOG_ICON + }, + [TraceType.SYSTEM_UI]: { + name: "System UI", + icon: SYSTEM_UI_ICON + }, + [TraceType.LAUNCHER]: { + name: "Launcher", + icon: LAUNCHER_ICON + }, + [TraceType.INPUT_METHOD_CLIENTS]: { + name: "IME Clients", + icon: IME_ICON + }, + [TraceType.INPUT_METHOD_SERVICE]: { + name: "IME Service", + icon: IME_ICON + }, + [TraceType.INPUT_METHOD_MANAGER_SERVICE]: { + name: "IME Manager Service", + icon: IME_ICON + }, + [TraceType.TAG]: { + name: "Tag", + icon: TAG_ICON + }, + [TraceType.ERROR]: { + name: "Error", + icon: TRACE_ERROR_ICON + }, +}; diff --git a/tools/winscope-ng/src/styles.css b/tools/winscope-ng/src/styles.css index feaa90e9f..120f7a884 100644 --- a/tools/winscope-ng/src/styles.css +++ b/tools/winscope-ng/src/styles.css @@ -16,11 +16,18 @@ @import "~@angular/material/prebuilt-themes/indigo-pink.css"; @import 'https://fonts.googleapis.com/icon?family=Material+Icons'; +#app-title { + font-weight: bold; + font-family: Arial, Helvetica, sans-serif; + color:rgb(194, 65, 108); + font-size: 30; +} + #title { font-weight: bold; font-family: Arial, Helvetica, sans-serif; color:rgb(194, 65, 108); - font-size: 20; + font-size: 24; } button { @@ -29,20 +36,53 @@ button { .homepage-card { border: 1px solid rgb(129, 129, 129); - width: 45rem; - height: 30rem; + width: 50%; + height: 35rem; overflow: auto; display: flex; margin: 10px; } -mat-checkbox { - margin-left: 10px; +.trace-card { + border: 1px solid rgb(129, 129, 129); + height: 100%; + overflow: auto; + display: flex; + margin: 10px; } -mat-form-field { - margin: 10px; - height: 5px; +.card-grid { + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + overflow: auto; +} + +mat-checkbox { + margin-left: 5px; +} + +.mat-checkbox .mat-checkbox-frame { + transform: scale(0.7); +} + +.mat-checkbox-checked .mat-checkbox-background { + transform: scale(0.7); +} + +.mat-checkbox-indeterminate .mat-checkbox-background { + transform: scale(0.7); +} + +.mat-radio-button, .mat-radio-button-frame { + transform: scale(0.8); +} + +.mat-form-field { + transform: scale(0.85); + margin: 2px; + padding: 0; } mat-icon { @@ -75,12 +115,12 @@ button.mat-raised-button { } .drop-box { - outline: 2px dashed rgb(194, 65, 108); /* the dash box */ + outline: 2px dashed rgb(194, 65, 108); outline-offset: -10px; background: white; color: rgb(194, 65, 108); padding: 10px 10px 10px 10px; - min-height: 200px; /* minimum height */ + min-height: 200px; position: relative; cursor: pointer; display: flex; @@ -104,4 +144,19 @@ button.mat-raised-button { background-color: rgb(194, 65, 108); border-radius: 21.5px; cursor: pointer; -} \ No newline at end of file +} + +.viewers.hide { + display: none !important; +} + +[hidden] { + display: none !important; +} + +.icon-button { + background: none; + border: none; + display: inline-block; + vertical-align: middle; +} diff --git a/tools/winscope-ng/src/test/e2e/viewer_surface_flinger.spec.ts b/tools/winscope-ng/src/test/e2e/viewer_surface_flinger.spec.ts new file mode 100644 index 000000000..fb82ee53c --- /dev/null +++ b/tools/winscope-ng/src/test/e2e/viewer_surface_flinger.spec.ts @@ -0,0 +1,35 @@ +/* + * 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, ElementFinder} from "protractor"; +import {E2eTestUtils} from "./utils"; + +describe("Viewer SurfaceFlinger", () => { + beforeAll(async () => { + browser.manage().timeouts().implicitlyWait(1000); + browser.get("file://" + E2eTestUtils.getProductionIndexHtmlPath()); + }), + + it("processes trace and renders view", () => { + const inputFile = element(by.css("input[type=\"file\"]")); + inputFile.sendKeys(E2eTestUtils.getFixturePath("traces/elapsed_and_real_timestamp/SurfaceFlinger.pb")); + + const loadData = element(by.css(".load-btn")); + loadData.click(); + + const surfaceFlingerCard: ElementFinder = element(by.css(".trace-card")); + expect(surfaceFlingerCard.getText()).toContain("Surface Flinger"); + }); +}); diff --git a/tools/winscope-ng/src/test/e2e/winscope.spec.ts b/tools/winscope-ng/src/test/e2e/winscope.spec.ts index ac7032ad1..bc22e930c 100644 --- a/tools/winscope-ng/src/test/e2e/winscope.spec.ts +++ b/tools/winscope-ng/src/test/e2e/winscope.spec.ts @@ -22,7 +22,7 @@ describe("winscope", () => { }), it("has title", () => { - const title = element(by.css("#title")); + const title = element(by.css("#app-title")); expect(title.getText()).toContain("Winscope"); }); }); \ No newline at end of file diff --git a/tools/winscope-ng/src/trace_collection/proxy_client.ts b/tools/winscope-ng/src/trace_collection/proxy_client.ts index b10d65e7a..e3ac3ff32 100644 --- a/tools/winscope-ng/src/trace_collection/proxy_client.ts +++ b/tools/winscope-ng/src/trace_collection/proxy_client.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { PersistentStore } from "../common/persistent_store"; +import { PersistentStore } from "common/persistent_store"; import { configMap, TRACES } from "./trace_collection_utils"; import { setTraces, SetTraces } from "./set_traces"; import { Device } from "./connection"; diff --git a/tools/winscope-ng/src/viewers/canvas_graphics.ts b/tools/winscope-ng/src/viewers/canvas_graphics.ts new file mode 100644 index 000000000..86d24d3e5 --- /dev/null +++ b/tools/winscope-ng/src/viewers/canvas_graphics.ts @@ -0,0 +1,416 @@ +/* + * 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 { Rectangle } from "viewers/viewer_surface_flinger/ui_data"; +import * as THREE from "three"; +import { CSS2DRenderer, CSS2DObject } from "three/examples/jsm/renderers/CSS2DRenderer"; + +export class CanvasGraphics { + constructor() { + //set up camera + const left = -this.cameraHalfWidth, + right = this.cameraHalfWidth, + top = this.cameraHalfHeight, + bottom = -this.cameraHalfHeight, + near = 0.001, + far = 100; + this.camera = new THREE.OrthographicCamera( + left,right,top,bottom,near,far + ); + } + + initialise(canvas: HTMLCanvasElement) { + // initialise canvas + this.canvas = canvas; + } + + refreshCanvas() { + //set canvas size + this.canvas!.style.width = "100%"; + this.canvas!.style.height = "40rem"; + + // TODO: click and drag rotation control + this.camera.position.set(this.xyCameraPos, this.xyCameraPos, 6); + this.camera.lookAt(0, 0, 0); + this.camera.zoom = this.camZoom; + this.camera.updateProjectionMatrix(); + + // scene + const scene = new THREE.Scene(); + + // renderers + const renderer = new THREE.WebGLRenderer({ + antialias: true, + canvas: this.canvas, + alpha: true + }); + let labelRenderer: CSS2DRenderer; + if (document.querySelector("#labels-canvas")) { + labelRenderer = new CSS2DRenderer({ + element: document.querySelector("#labels-canvas")! as HTMLElement + }); + } else { + labelRenderer = new CSS2DRenderer(); + labelRenderer.domElement.style.position = "absolute"; + labelRenderer.domElement.style.top = "0px"; + labelRenderer.domElement.style.width = "100%"; + labelRenderer.domElement.style.height = "40rem"; + labelRenderer.domElement.id = "labels-canvas"; + labelRenderer.domElement.style.pointerEvents = "none"; + document.querySelector(".canvas-container")?.appendChild(labelRenderer.domElement); + } + + // set various factors for shading and shifting + const visibleDarkFactor = 0, nonVisibleDarkFactor = 0, rectCounter = 0; + const numberOfRects = this.rects.length; + const numberOfVisibleRects = this.rects.filter(rect => rect.isVisible).length; + const numberOfDisplayRects = this.rects.filter(rect => rect.isDisplay).length; + + const zShift = numberOfRects*this.layerSeparation; + let xShift = 0, yShift = 3.25, labelYShift = 0; + + if (this.isLandscape) { + xShift = 1; + yShift = 1.5; + labelYShift = 1.25; + } + + const lowestY = Math.min(...this.rects.map(rect => { + const y = rect.topLeft.y - rect.height + this.lowestYShift; + if (this.isLandscape) { + if (y<0) { + return 0; + } else if (y > 2) { + return 2; + } + } else if (y > -1) { + return -1; + } + return y; + })) - labelYShift; + + this.drawScene( + rectCounter, + numberOfVisibleRects, + visibleDarkFactor, + numberOfDisplayRects, + nonVisibleDarkFactor, + numberOfRects, + scene, + xShift, + yShift, + zShift, + lowestY + ); + + // const axesHelper = new THREE.AxesHelper(1); + // const gridHelper = new THREE.GridHelper(5); + // scene.add(axesHelper, gridHelper) + + renderer.setSize(this.canvas!.clientWidth, this.canvas!.clientHeight); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.render(scene, this.camera); + + labelRenderer.setSize(this.canvas!.clientWidth, this.canvas!.clientHeight); + labelRenderer.render(scene, this.camera); + } + + private drawScene( + rectCounter: number, + visibleRects: number, + visibleDarkFactor:number, + displayRects: number, + nonVisibleDarkFactor: number, + numberOfRects: number, + scene: THREE.Scene, + xShift: number, + yShift: number, + zShift: number, + lowestY: number + ) { + this.targetObjects = []; + this.rects.forEach(rect => { + const visibleViewInvisibleRect = this.visibleView && !rect.isVisible; + const xrayViewNoVirtualDisplaysVirtualRect = !this.visibleView && !this.showVirtualDisplays && rect.isDisplay && rect.isVirtual; + if (visibleViewInvisibleRect || xrayViewNoVirtualDisplaysVirtualRect) { + rectCounter++; + return; + } + + //set colour mapping + let planeColor; + if (this.highlighted === `${rect.id}`) { + planeColor = this.colorMapping("highlight", numberOfRects, 0); + } else if (rect.isVisible) { + planeColor = this.colorMapping("green", visibleRects, visibleDarkFactor); + visibleDarkFactor++; + } else if (rect.isDisplay) { + planeColor = this.colorMapping("grey", displayRects, nonVisibleDarkFactor); + nonVisibleDarkFactor++; + } else { + planeColor = this.colorMapping("unknown", numberOfRects, 0); + } + + //set plane geometry and material + const geometry = new THREE.PlaneGeometry(rect.width, rect.height); + const planeRect = this.setPlaneMaterial(rect, geometry, planeColor, xShift, yShift, zShift); + scene.add(planeRect); + zShift -= this.layerSeparation; + + // bolder edges of each plane if in x-ray view + if (!this.visibleView) { + const edgeSegments = this.setEdgeMaterial(planeRect, geometry); + scene.add(edgeSegments); + } + + // label circular marker + const circle = this.setCircleMaterial(planeRect, rect); + scene.add(circle); + this.targetObjects.push(planeRect); + + // label line + const [line, rectLabel] = this.createLabel(rect, circle, lowestY, rectCounter); + scene.add(line); + scene.add(rectLabel); + rectCounter++; + }); + } + + private setPlaneMaterial( + rect: Rectangle, + geometry: THREE.PlaneGeometry, + color: THREE.Color, + xShift: number, + yShift: number, + zShift: number + ) { + const planeRect = new THREE.Mesh( + geometry, + new THREE.MeshBasicMaterial({ + color: color, + opacity: this.visibleView ? 1 : 0.75, + transparent: true, + })); + planeRect.position.y = rect.topLeft.y - rect.height/2 + yShift; + planeRect.position.x = rect.topLeft.x + rect.width/2 - xShift; + planeRect.position.z = zShift; + planeRect.name = `${rect.id}`; + return planeRect; + } + + private setEdgeMaterial(planeRect: THREE.Mesh, geometry: THREE.PlaneGeometry) { + const edgeColor = 0x000000; + const edgeGeo = new THREE.EdgesGeometry(geometry); + const edgeMaterial = new THREE.LineBasicMaterial({color: edgeColor, linewidth: 1}); + const edgeSegments = new THREE.LineSegments( + edgeGeo, edgeMaterial + ); + edgeSegments.position.set(planeRect.position.x, planeRect.position.y, planeRect.position.z); + return edgeSegments; + } + + private setCircleMaterial(planeRect: THREE.Mesh, rect: Rectangle) { + const labelCircle = new THREE.CircleGeometry(0.02, 200); + const circleMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 }); + const circle = new THREE.Mesh(labelCircle, circleMaterial); + circle.position.set( + planeRect.position.x + rect.width/2 - 0.05, + planeRect.position.y, + planeRect.position.z + 0.05 + ); + circle.rotateY(THREE.MathUtils.degToRad(30)); + return circle; + } + + private createLabel(rect: Rectangle, circle: THREE.Mesh, lowestY: number, rectCounter: number): + [THREE.Line, CSS2DObject] { + const labelText = this.shortenText(rect.label); + const isGrey = !this.visibleView && !rect.isVisible; + let cornerPos, endPos; + const labelYSeparation = 0.3; + if (this.isLandscape) { + cornerPos = new THREE.Vector3( + circle.position.x, lowestY - 0.5 - rectCounter*labelYSeparation, circle.position.z + ); + } else { + cornerPos = new THREE.Vector3( + circle.position.x, lowestY + 0.5 - rectCounter*labelYSeparation, circle.position.z + ); + } + + const linePoints = [circle.position, cornerPos]; + if (this.isLandscape && cornerPos.x > 0 || !this.isLandscape) { + endPos = new THREE.Vector3(cornerPos.x - 1, cornerPos.y - this.labelShift, cornerPos.z); + } else { + endPos = cornerPos; + } + linePoints.push(endPos); + + //add rectangle label + document.querySelector(`.label-${rectCounter}`)?.remove(); + const rectLabelDiv: HTMLElement = document.createElement("div"); + this.labelElements.push(rectLabelDiv); + rectLabelDiv.className = `label-${rectCounter}`; + rectLabelDiv.textContent = labelText; + rectLabelDiv.style.fontSize = "10px"; + if (isGrey) { + rectLabelDiv.style.color = "grey"; + } + const rectLabel = new CSS2DObject(rectLabelDiv); + rectLabel.name = rect.label; + + const textCanvas = document.createElement("canvas"); + const labelContext = textCanvas.getContext("2d"); + let labelWidth = 0; + if (labelContext?.font) { + labelContext.font = rectLabelDiv.style.font; + labelWidth = labelContext?.measureText(labelText).width; + } + textCanvas.remove(); + + if (this.isLandscape && endPos.x < 0) { + rectLabel.position.set( + endPos.x + 0.6, endPos.y - 0.15, endPos.z - 0.6 + ); + } else { + rectLabel.position.set( + endPos.x - labelWidth * this.labelXFactor, endPos.y - this.labelShift * labelWidth * this.labelXFactor, endPos.z + ); + } + + const lineGeo = new THREE.BufferGeometry().setFromPoints(linePoints); + const lineMaterial = new THREE.LineBasicMaterial({color: isGrey ? 0x808080 : 0x000000}); + const line = new THREE.Line(lineGeo, lineMaterial); + + return [line, rectLabel]; + } + + getCamera() { + return this.camera; + } + + getTargetObjects() { + return this.targetObjects; + } + + getLayerSeparation() { + return this.layerSeparation; + } + + getVisibleView() { + return this.visibleView; + } + + getXyCameraPos() { + return this.xyCameraPos; + } + + getShowVirtualDisplays() { + return this.showVirtualDisplays; + } + + updateLayerSeparation(userInput: number) { + this.layerSeparation = userInput; + } + + updateRotation(userInput: number) { + this.xyCameraPos = userInput; + this.camZoom = userInput/4 * 0.2 + 0.9; + this.labelShift = userInput/4 * this.maxLabelShift; + this.lowestYShift = userInput/4 + 2; + } + + updateHighlighted(highlighted: string) { + this.highlighted = highlighted; + } + + updateRects(rects: Rectangle[]) { + this.rects = rects; + } + + updateIsLandscape(isLandscape: boolean) { + this.isLandscape = isLandscape; + } + + updateVisibleView(visible: boolean) { + this.visibleView = visible; + } + + updateVirtualDisplays(show: boolean) { + this.showVirtualDisplays = show; + } + + clearLabelElements() { + this.labelElements.forEach(el => el.remove()); + } + + updateZoom(isZoomIn: boolean) { + if (isZoomIn && this.camZoom < 2) { + this.camZoom += this.camZoomFactor * 1.5; + } else if (!isZoomIn && this.camZoom > 0.5) { + this.camZoom -= this.camZoomFactor * 1.5; + } + } + + colorMapping(scale: string, numberOfRects: number, darkFactor:number): THREE.Color { + if (scale === "highlight") { + return new THREE.Color(0xD2E3FC); + } else if (scale === "grey") { + // darkness of grey rect depends on z order - darkest 64, lightest 128 + //Separate RGB values between 0 and 1 + const lower = 120; + const upper = 220; + const darkness = ((upper-lower)*(numberOfRects-darkFactor)/numberOfRects + lower)/255; + return new THREE.Color(darkness, darkness, darkness); + } else if (scale === "green") { + // darkness of green rect depends on z order + //Separate RGB values between 0 and 1 + const red = ((200-45)*(numberOfRects-darkFactor)/numberOfRects + 45)/255; + const green = ((232-182)*(numberOfRects-darkFactor)/numberOfRects + 182)/255; + const blue = ((183-44)*(numberOfRects-darkFactor)/numberOfRects + 44)/255; + return new THREE.Color(red, green, blue); + } else { + return new THREE.Color(0, 0, 0); + } + } + + shortenText(text: string): string { + if (text.length > 40) { + text = text.slice(0, 40); + } + return text; + } + + // dynamic scaling and canvas variables + readonly cameraHalfWidth = 2.8; + readonly cameraHalfHeight = 3.2; + private readonly maxLabelShift = 0.305; + private readonly labelXFactor = 0.008; + private lowestYShift = 3; + private camZoom = 1.1; + private camZoomFactor = 0.1; + private labelShift = this.maxLabelShift; + private highlighted = ""; + private visibleView = false; + private isLandscape = false; + private showVirtualDisplays = false; + private layerSeparation = 0.4; + private xyCameraPos = 4; + private camera: THREE.OrthographicCamera; + private rects: Rectangle[] = []; + private labelElements: HTMLElement[] = []; + private targetObjects: any[] = []; + private canvas?: HTMLCanvasElement; +} diff --git a/tools/winscope-ng/src/viewers/hierarchy.component.ts b/tools/winscope-ng/src/viewers/hierarchy.component.ts new file mode 100644 index 000000000..386a1dad0 --- /dev/null +++ b/tools/winscope-ng/src/viewers/hierarchy.component.ts @@ -0,0 +1,29 @@ +/* + * 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 } from "@angular/core"; + +@Component({ + selector: "hierarchy-view", + template: ` + Hierarchy + `, + styles: [ + ".trace-view-subtitle { font-size: 18px}" + ] +}) + +export class HierarchyComponent { +} diff --git a/tools/winscope-ng/src/viewers/properties.component.ts b/tools/winscope-ng/src/viewers/properties.component.ts new file mode 100644 index 000000000..92fc1ff1c --- /dev/null +++ b/tools/winscope-ng/src/viewers/properties.component.ts @@ -0,0 +1,26 @@ +/* + * 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 } from "@angular/core"; + +@Component({ + selector: "properties-view", + template: ` + Properties + `, +}) + +export class PropertiesComponent { +} diff --git a/tools/winscope-ng/src/viewers/rects.component.spec.ts b/tools/winscope-ng/src/viewers/rects.component.spec.ts new file mode 100644 index 000000000..1643e889d --- /dev/null +++ b/tools/winscope-ng/src/viewers/rects.component.spec.ts @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { CommonModule } from "@angular/common"; +import { Component , ViewChild } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { RectsComponent } from "./rects.component"; +import { MatCheckboxModule } from "@angular/material/checkbox"; +import { MatCardModule } from "@angular/material/card"; +import { MatRadioModule } from "@angular/material/radio"; +import { MatSliderModule } from "@angular/material/slider"; +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { Rectangle } from "./viewer_surface_flinger/ui_data"; + +describe("RectsComponent", () => { + let component: TestHostComponent; + let fixture: ComponentFixture; + let htmlElement: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + CommonModule, + MatCheckboxModule, + MatCardModule, + MatSliderModule, + MatRadioModule + ], + declarations: [RectsComponent, TestHostComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TestHostComponent); + component = fixture.componentInstance; + htmlElement = fixture.nativeElement; + fixture.detectChanges(); + }); + + it("can be created", () => { + expect(component).toBeTruthy(); + }); + + it("check that layer separation slider is rendered", () => { + fixture.detectChanges(); + const slider = htmlElement.querySelector("mat-slider"); + expect(slider).toBeTruthy(); + }); + + it("check that layer separation slider causes view to change", () => { + const slider = htmlElement.querySelector("mat-slider"); + spyOn(component.rectsComponent.canvasGraphics, "updateLayerSeparation"); + slider?.dispatchEvent(new MouseEvent("mousedown")); + fixture.detectChanges(); + expect(component.rectsComponent.canvasGraphics.updateLayerSeparation).toHaveBeenCalled(); + }); + + it("check that rects canvas is rendered", () => { + fixture.detectChanges(); + const rectsCanvas = htmlElement.querySelector("#rects-canvas"); + expect(rectsCanvas).toBeTruthy(); + }); + + it("check that canvas is refreshed if rects are present", async () => { + component.addRects([ + { + topLeft: {x:0, y:0}, + bottomRight: {x:1, y:-1}, + label: "rectangle1", + transform: { + matrix: { + dsdx: 1, + dsdy: 0, + dtdx: 0, + dtdy: 1, + tx: 0, + ty: 0 + } + }, + height: 1, + width: 1, + isVisible: true, + isDisplay: false, + ref: null, + id: 12345, + stackId: 0, + } + ]); + spyOn(component.rectsComponent, "drawRects").and.callThrough(); + fixture.detectChanges(); + expect(component.rectsComponent.drawRects).toHaveBeenCalled(); + }); + + @Component({ + selector: "host-component", + template: "" + }) + class TestHostComponent { + public rects: Rectangle[] = []; + + addRects(newRects: Rectangle[]) { + this.rects = newRects; + } + + @ViewChild(RectsComponent) + public rectsComponent!: RectsComponent; + } +}); diff --git a/tools/winscope-ng/src/viewers/rects.component.ts b/tools/winscope-ng/src/viewers/rects.component.ts new file mode 100644 index 000000000..b8a10e97a --- /dev/null +++ b/tools/winscope-ng/src/viewers/rects.component.ts @@ -0,0 +1,286 @@ +/* + * 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, Input, OnChanges, OnDestroy, Inject, ElementRef, SimpleChanges } from "@angular/core"; +import { RectsUtils } from "./rects_utils"; +import { Point, Rectangle, RectMatrix, RectTransform } from "viewers/viewer_surface_flinger/ui_data"; +import { interval, Subscription } from "rxjs"; +import { CanvasGraphics } from "./canvas_graphics"; +import * as THREE from "three"; + +@Component({ + selector: "rects-view", + template: ` + + + Visible + X-ray + + + + Show virtual displays + + +
+
+ + +
+ + +
+
+ `, + styles: [ + "@import 'https://fonts.googleapis.com/icon?family=Material+Icons';", + ".rects-content {position: relative}", + ".canvas-container {height: 40rem; width: 100%; position: relative}", + "#rects-canvas {height: 40rem; width: 100%; cursor: pointer; position: absolute; top: 0px}", + "#labels-canvas {height: 40rem; width: 100%; position: absolute; top: 0px}", + ".view-controls {display: inline-block; position: relative; min-height: 72px}", + ".zoom-container {position: absolute; top: 0px; z-index: 10}", + "#zoom-btn {position:relative; display: block; background: none; border: none}", + "mat-radio-button {font-size: 16px; font-weight: normal}", + ".mat-radio-button, .mat-radio-button-frame {transform: scale(0.8);}", + ".rects-checkbox {font-size: 14px; font-weight: normal}", + "mat-icon {margin: 5px}", + "mat-checkbox {margin-left: 5px;}", + ".mat-checkbox .mat-checkbox-frame { transform: scale(0.7);}", + ".mat-checkbox-checked .mat-checkbox-background {transform: scale(0.7);}", + ".mat-checkbox-indeterminate .mat-checkbox-background {transform: scale(0.7);}", + ] +}) + +export class RectsComponent implements OnChanges, OnDestroy { + @Input() rects!: Rectangle[]; + + @Input() highlighted = ""; + + constructor( + @Inject(ElementRef) private elementRef: ElementRef, + ) { + this.canvasGraphics = new CanvasGraphics(); + } + + ngOnDestroy() { + if (this.canvasSubscription) { + this.canvasSubscription.unsubscribe(); + } + } + + ngOnChanges(changes: SimpleChanges) { + if (this.rects.length > 0) { + //change in rects so they must undergo transformation and scaling before canvas refreshed + this.canvasGraphics.clearLabelElements(); + this.rects = this.rects.filter(rect => rect.isVisible || rect.isDisplay); + this.displayRects = this.rects.filter(rect => rect.isDisplay); + this.computeBounds(); + this.rects = this.rects.map(rect => { + if (changes["rects"] && rect.transform) { + return RectsUtils.transformRect(rect.transform.matrix ?? rect.transform, rect); + } else { + return rect; + } + }); + this.scaleRects(); + this.drawRects(); + } else if (this.canvasSubscription) { + this.canvasSubscription.unsubscribe(); + } + } + + onRectClick(event:PointerEvent) { + this.setNormalisedMousePos(event); + const raycaster = new THREE.Raycaster(); + raycaster.setFromCamera(this.mouse, this.canvasGraphics.getCamera()); + // create an array containing all objects in the scene with which the ray intersects + const intersects = raycaster.intersectObjects(this.canvasGraphics.getTargetObjects()); + // if there is one (or more) intersections + if (intersects.length > 0){ + if (this.highlighted === intersects[0].object.name) { + this.highlighted = ""; + this.canvasGraphics.updateHighlighted(""); + } else { + this.highlighted = intersects[0].object.name; + this.canvasGraphics.updateHighlighted(intersects[0].object.name); + } + this.updateHighlightedRect(); + } + } + + setNormalisedMousePos(event:PointerEvent) { + event.preventDefault(); + const canvas = (event.target as Element); + const canvasOffset = canvas.getBoundingClientRect(); + this.mouse.x = ((event.clientX-canvasOffset.left)/canvas.clientWidth) * 2 - 1; + this.mouse.y = -((event.clientY-canvasOffset.top)/canvas.clientHeight) * 2 + 1; + this.mouse.z = 0; + } + + updateHighlightedRect() { + const event: CustomEvent = new CustomEvent("highlightedChange", { + bubbles: true, + detail: { layerId: this.highlighted } + }); + this.elementRef.nativeElement.dispatchEvent(event); + } + + drawRects() { + if (this.canvasSubscription) { + this.canvasSubscription.unsubscribe(); + } + const canvas = document.getElementById("rects-canvas") as HTMLCanvasElement; + this.canvasGraphics.initialise(canvas); + this.canvasSubscription = this.drawRectsInterval.subscribe(() => { + this.updateVariablesBeforeRefresh(); + this.canvasGraphics.refreshCanvas(); + }); + } + + updateVariablesBeforeRefresh() { + this.canvasGraphics.updateRects(this.rects); + const biggestX = Math.max(...this.rects.map(rect => rect.topLeft.x + rect.width/2)); + this.canvasGraphics.updateIsLandscape(biggestX > this.s({x: this.boundsWidth, y:this.boundsHeight}).x/2); + } + + onChangeView(visible: boolean) { + this.canvasGraphics.updateVisibleView(visible); + this.canvasGraphics.clearLabelElements(); + } + + scaleRects() { + this.rects = this.rects.map(rect => { + rect.bottomRight = this.s(rect.bottomRight); + rect.topLeft = this.s(rect.topLeft); + rect.height = Math.abs(rect.topLeft.y - rect.bottomRight.y); + rect.width = Math.abs(rect.bottomRight.x - rect.topLeft.x); + const mat = this.getMatrix(rect); + if (mat) { + const newTranslation = this.s({x: mat.tx!, y: mat.ty!}); + mat.tx = newTranslation.x; + mat.ty = newTranslation.y; + } + return rect; + }); + } + + computeBounds(): any { + this.boundsWidth = Math.max(...this.rects.map((rect) => { + const mat = this.getMatrix(rect); + if (mat) { + return RectsUtils.transformRect(mat, rect).width; + } else { + return rect.width; + }})); + this.boundsHeight = Math.max(...this.rects.map((rect) => { + const mat = this.getMatrix(rect); + if (mat) { + return RectsUtils.transformRect(mat, rect).height; + } else { + return rect.height; + }})); + + if (this.displayRects.length > 0) { + this.boundsWidth = Math.min(this.boundsWidth, this.maxWidth()); + this.boundsHeight = Math.min(this.boundsHeight, this.maxHeight()); + } + } + + maxWidth() { + return Math.max(...this.displayRects.map(rect => rect.width)) * 1.2; + } + + maxHeight() { + return Math.max(...this.displayRects.map(rect => rect.height)) * 1.2; + } + + // scales coordinates to canvas + s(sourceCoordinates: Point) { + let scale; + if (this.boundsWidth < this.boundsHeight) { + scale = this.canvasGraphics.cameraHalfHeight*2 * 0.6 / this.boundsHeight; + } else { + scale = this.canvasGraphics.cameraHalfWidth*2 * 0.6 / this.boundsWidth; + } + return { + x: sourceCoordinates.x * scale, + y: sourceCoordinates.y * scale, + }; + } + + getMatrix(rect: Rectangle) { + if (rect.transform) { + let matrix: RectTransform | RectMatrix = rect.transform; + if (rect.transform && rect.transform.matrix) { + matrix = rect.transform.matrix; + } + return matrix; + } else { + return false; + } + } + + visibleView() { + return this.canvasGraphics.getVisibleView(); + } + + getLayerSeparation() { + return this.canvasGraphics.getLayerSeparation(); + } + + xyCameraPos() { + return this.canvasGraphics.getXyCameraPos(); + } + + showVirtualDisplays() { + return this.canvasGraphics.getShowVirtualDisplays(); + } + + canvasGraphics: CanvasGraphics; + private readonly _60fpsInterval = 16.66666666666667; + private drawRectsInterval = interval(this._60fpsInterval); + private boundsWidth = 0; + private boundsHeight = 0; + private displayRects!: Rectangle[]; + private canvasSubscription?: Subscription; + private mouse = new THREE.Vector3(0, 0, 0); +} diff --git a/tools/winscope-ng/src/viewers/rects_utils.spec.ts b/tools/winscope-ng/src/viewers/rects_utils.spec.ts new file mode 100644 index 000000000..310b4f480 --- /dev/null +++ b/tools/winscope-ng/src/viewers/rects_utils.spec.ts @@ -0,0 +1,59 @@ +/* + * 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 { RectsUtils } from "./rects_utils"; + +describe("RectsUtils", () => { + it("transforms rect", () => { + const transform = { + matrix: { + dsdx: 1, + dsdy: 0, + dtdx: 0, + dtdy: 1, + tx: 1, + ty: 1 + } + }; + const rect = { + topLeft: {x: 0, y: 0}, + bottomRight: {x: 1, y: -1}, + label: "TestRect", + transform: transform, + isVisible: true, + isDisplay: false, + height: 1, + width: 1, + ref: null, + id: 12345, + stackId: 0 + }; + const expected = { + topLeft: {x: 1, y: 1}, + bottomRight: {x: 2, y: 0}, + label: "TestRect", + transform: transform, + isVisible: true, + isDisplay: false, + height: 1, + width: 1, + ref: null, + id: 12345, + stackId: 0, + isVirtual: undefined + }; + expect(RectsUtils.transformRect(rect.transform.matrix, rect)).toEqual(expected); + }); +}); diff --git a/tools/winscope-ng/src/viewers/rects_utils.ts b/tools/winscope-ng/src/viewers/rects_utils.ts new file mode 100644 index 000000000..7306ab4a6 --- /dev/null +++ b/tools/winscope-ng/src/viewers/rects_utils.ts @@ -0,0 +1,60 @@ +/* + * 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 { Point, Rectangle, RectMatrix, RectTransform } from "viewers/viewer_surface_flinger/ui_data"; + +export const RectsUtils = { + multiplyMatrix(matrix:any, corner: Point): Point { + if (!matrix) return corner; + // |dsdx dsdy tx| | x | |x*dsdx + y*dsdy + tx| + // |dtdx dtdy ty| x | y | = |x*dtdx + y*dtdy + ty| + // |0 0 1 | | 1 | | 1 | + return { + x: matrix.dsdx * corner.x + matrix.dsdy * corner.y + matrix.tx, + y: matrix.dtdx * corner.x + matrix.dtdy * corner.y + matrix.ty, + }; + }, + + transformRect(matrix: RectMatrix | RectTransform, rect:Rectangle): Rectangle { + // | dsdx dsdy tx | | left top 1 | + // matrix = | dtdx dtdy ty | rect = | 1 1 1 | + // | 0 0 1 | | 1 right bottom | + const tl = this.multiplyMatrix(matrix, rect.topLeft); + const tr = this.multiplyMatrix(matrix, {x:rect.bottomRight.x, y:rect.topLeft.y}); + const bl = this.multiplyMatrix(matrix, {x:rect.topLeft.x, y:rect.bottomRight.y}); + const br = this.multiplyMatrix(matrix, rect.bottomRight); + + const left = Math.min(tl.x, tr.x, bl.x, br.x); + const top = Math.max(tl.y, tr.y, bl.y, br.y); + const right = Math.max(tl.x, tr.x, bl.x, br.x); + const bottom = Math.min(tl.y, tr.y, bl.y, br.y); + + const outrect: Rectangle = { + topLeft: {x: left, y: top}, + bottomRight: {x: right, y: bottom}, + label: rect.label, + transform: rect.transform, + isVisible: rect.isVisible, + isDisplay: rect.isDisplay, + height: Math.abs(top - bottom), + width: Math.abs(right - left), + ref: rect.ref, + id: rect.id, + stackId: rect.stackId, + isVirtual: rect.isVirtual + }; + return outrect; + } +}; diff --git a/tools/winscope-ng/src/viewers/viewer.ts b/tools/winscope-ng/src/viewers/viewer.ts index 5c66d09b2..5ad1857e0 100644 --- a/tools/winscope-ng/src/viewers/viewer.ts +++ b/tools/winscope-ng/src/viewers/viewer.ts @@ -19,6 +19,8 @@ interface Viewer { //TODO: add TraceEntry data type notifyCurrentTraceEntries(entries: Map): void; getView(): HTMLElement; + getTitle(): string; + getDependencies(): TraceType[]; } export { Viewer }; diff --git a/tools/winscope-ng/src/viewers/viewer_factory.ts b/tools/winscope-ng/src/viewers/viewer_factory.ts index 82cf1e3c9..db9377d5d 100644 --- a/tools/winscope-ng/src/viewers/viewer_factory.ts +++ b/tools/winscope-ng/src/viewers/viewer_factory.ts @@ -16,10 +16,12 @@ import { TraceType } from "common/trace/trace_type"; import { Viewer } from "./viewer"; import { ViewerWindowManager } from "./viewer_window_manager/viewer_window_manager"; +import { ViewerSurfaceFlinger } from "./viewer_surface_flinger/viewer_surface_flinger"; class ViewerFactory { static readonly VIEWERS = [ ViewerWindowManager, + ViewerSurfaceFlinger ]; public createViewers(activeTraceTypes: Set): Viewer[] { @@ -29,7 +31,6 @@ class ViewerFactory { const areViewerDepsSatisfied = Viewer.DEPENDENCIES.every((traceType: TraceType) => activeTraceTypes.has(traceType) ); - if (areViewerDepsSatisfied) { viewers.push(new Viewer()); } diff --git a/tools/winscope-ng/src/viewers/viewer_surface_flinger/presenter.ts b/tools/winscope-ng/src/viewers/viewer_surface_flinger/presenter.ts new file mode 100644 index 000000000..ff5c0c264 --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_surface_flinger/presenter.ts @@ -0,0 +1,108 @@ +/* + * 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 { Rectangle, RectMatrix, RectTransform, UiData } from "viewers/viewer_surface_flinger/ui_data"; +import { TraceType } from "common/trace/trace_type"; + +type NotifyViewCallbackType = (uiData: UiData) => void; + +class Presenter { + constructor(notifyViewCallback: NotifyViewCallbackType) { + this.notifyViewCallback = notifyViewCallback; + this.uiData = new UiData("Initial UI data"); + this.notifyViewCallback(this.uiData); + } + + updateHighlightedRect(event: CustomEvent) { + this.highlighted = event.detail.layerId; + this.uiData.highlighted = this.highlighted; + console.log("changed highlighted rect: ", this.uiData.highlighted); + this.notifyViewCallback(this.uiData); + } + + notifyCurrentTraceEntries(entries: Map) { + const entry = entries.get(TraceType.SURFACE_FLINGER); + this.uiData = new UiData("New surface flinger ui data"); + const displayRects = entry.displays.map((display: any) => { + const rect = display.layerStackSpace; + rect.label = display.name; + rect.id = display.id; + rect.stackId = display.layerStackId; + rect.isDisplay = true; + rect.isVirtual = display.isVirtual; + return rect; + }) ?? []; + this.uiData.highlighted = this.highlighted; + this.uiData.rects = this.rectsToUiData(entry.rects.concat(displayRects)); + this.notifyViewCallback(this.uiData); + } + + rectsToUiData(rects: any[]): Rectangle[] { + const uiRects: Rectangle[] = []; + rects.forEach((rect: any) => { + let t = null; + if (rect.transform && rect.transform.matrix) { + t = rect.transform.matrix; + } else if (rect.transform) { + t = rect.transform; + } + let transform: RectTransform | null = null; + if (t !== null) { + const matrix: RectMatrix = { + dsdx: t.dsdx, + dsdy: t.dsdy, + dtdx: t.dtdx, + dtdy: t.dtdy, + tx: t.tx, + ty: -t.ty + }; + transform = { + matrix: matrix, + }; + } + + let isVisible = false, isDisplay = false; + if (rect.ref && rect.ref.isVisible) { + isVisible = rect.ref.isVisible; + } + if (rect.isDisplay) { + isDisplay = rect.isDisplay; + } + + const newRect: Rectangle = { + topLeft: {x: rect.left, y: rect.top}, + bottomRight: {x: rect.right, y: -rect.bottom}, + height: rect.height, + width: rect.width, + label: rect.label, + transform: transform, + isVisible: isVisible, + isDisplay: isDisplay, + ref: rect.ref, + id: rect.id ?? rect.ref.id, + stackId: rect.stackId ?? rect.ref.stackId, + isVirtual: rect.isVirtual + }; + uiRects.push(newRect); + }); + return uiRects; + } + + private readonly notifyViewCallback: NotifyViewCallbackType; + private uiData: UiData; + private highlighted = ""; +} + +export {Presenter}; diff --git a/tools/winscope-ng/src/viewers/viewer_surface_flinger/ui_data.ts b/tools/winscope-ng/src/viewers/viewer_surface_flinger/ui_data.ts new file mode 100644 index 000000000..ec1796c01 --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_surface_flinger/ui_data.ts @@ -0,0 +1,63 @@ +/* + * 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. + */ +class UiData { + constructor(public text: string) { + console.log(text); + } + rects?: Rectangle[] = []; + highlighted?: string = ""; +} + +export interface Rectangle { + topLeft: Point; + bottomRight: Point; + label: string; + transform: RectTransform | null; + height: number; + width: number; + isVisible: boolean; + isDisplay: boolean; + ref: any; + id: number; + stackId: number; + isVirtual?: boolean; +} + +export interface Point { + x: number, + y: number +} + +export interface RectTransform { + matrix?: RectMatrix; + dsdx?: number; + dsdy?: number; + dtdx?: number; + dtdy?: number; + tx?: number; + ty?: number; +} + +export interface RectMatrix { + dsdx: number; + dsdy: number; + dtdx: number; + dtdy: number; + tx: number; + ty: number; +} + +export {UiData}; diff --git a/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.spec.ts b/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.spec.ts new file mode 100644 index 000000000..f4aa30284 --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.spec.ts @@ -0,0 +1,75 @@ +/* + * 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 {ComponentFixture, TestBed} from "@angular/core/testing"; +import {ViewerSurfaceFlingerComponent} from "./viewer_surface_flinger.component"; + +import { HierarchyComponent } from "viewers/hierarchy.component"; +import { PropertiesComponent } from "viewers/properties.component"; +import { RectsComponent } from "viewers/rects.component"; +import { MatIconModule } from "@angular/material/icon"; +import { MatCardModule } from "@angular/material/card"; +import { ComponentFixtureAutoDetect } from "@angular/core/testing"; +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; + +describe("ViewerSurfaceFlingerComponent", () => { + let fixture: ComponentFixture; + let component: ViewerSurfaceFlingerComponent; + let htmlElement: HTMLElement; + + beforeAll(async () => { + await TestBed.configureTestingModule({ + providers: [ + { provide: ComponentFixtureAutoDetect, useValue: true } + ], + imports: [ + MatIconModule, + MatCardModule + ], + declarations: [ + ViewerSurfaceFlingerComponent, + HierarchyComponent, + PropertiesComponent, + RectsComponent + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ViewerSurfaceFlingerComponent); + component = fixture.componentInstance; + htmlElement = fixture.nativeElement; + }); + + it("can be created", () => { + expect(component).toBeTruthy(); + }); + + it("creates rects view", () => { + const rectsView = htmlElement.querySelector(".rects-view"); + expect(rectsView).toBeTruthy(); + }); + + it("creates hierarchy view", () => { + const hierarchyView = htmlElement.querySelector("#sf-hierarchy-view"); + expect(hierarchyView).toBeTruthy(); + }); + + it("creates properties view", () => { + const propertiesView = htmlElement.querySelector("#sf-properties-view"); + expect(propertiesView).toBeTruthy(); + }); +}); diff --git a/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.ts b/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.ts new file mode 100644 index 000000000..7017441aa --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.ts @@ -0,0 +1,60 @@ +/* + * 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, + Input +} from "@angular/core"; +import { UiData } from "./ui_data"; +import { TRACE_INFO } from "app/trace_info"; +import { TraceType } from "common/trace/trace_type"; + +@Component({ + selector: "viewer-surface-flinger", + template: ` +
+ + + + + + + + + +
+ `, + styles: [ + "@import 'https://fonts.googleapis.com/icon?family=Material+Icons';", + "mat-icon {margin: 5px}", + "viewer-surface-flinger {font-family: Arial, Helvetica, sans-serif;}", + ".trace-card-title {display: inline-block; vertical-align: middle;}", + ".header-button {background: none; border: none; display: inline-block; vertical-align: middle;}", + ".card-grid {width: 100%;height: 100%;display: flex;flex-direction: row;overflow: auto;}", + ".rects-view {font: inherit; flex: none !important;width: 400px;margin: 8px;}", + ".hierarchy-view, .properties-view {font: inherit; flex: 1;margin: 8px;min-width: 400px;min-height: 50rem;max-height: 50rem;}", + ] +}) +export class ViewerSurfaceFlingerComponent { + @Input() + inputData?: UiData; + + TRACE_INFO = TRACE_INFO; + TraceType = TraceType; +} 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 new file mode 100644 index 000000000..5d49f6edd --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts @@ -0,0 +1,51 @@ +/* + * 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 {Viewer} from "viewers/viewer"; +import {Presenter} from "./presenter"; +import {UiData} from "./ui_data"; + +class ViewerSurfaceFlinger implements Viewer { + constructor() { + this.view = document.createElement("viewer-surface-flinger"); + this.presenter = new Presenter((uiData: UiData) => { + (this.view as any).inputData = uiData; + }); + this.view.addEventListener("highlightedChange", (event) => this.presenter.updateHighlightedRect((event as CustomEvent))); + } + + public notifyCurrentTraceEntries(entries: Map): void { + this.presenter.notifyCurrentTraceEntries(entries); + } + + public getView(): HTMLElement { + return this.view; + } + + public getTitle(): string { + return "Surface Flinger"; + } + + public getDependencies(): TraceType[] { + return ViewerSurfaceFlinger.DEPENDENCIES; + } + + public static readonly DEPENDENCIES: TraceType[] = [TraceType.SURFACE_FLINGER]; + private view: HTMLElement; + private presenter: Presenter; +} + +export {ViewerSurfaceFlinger}; diff --git a/tools/winscope-ng/src/viewers/viewer_window_manager/presenter.ts b/tools/winscope-ng/src/viewers/viewer_window_manager/presenter.ts index 30bf20fd9..04f407236 100644 --- a/tools/winscope-ng/src/viewers/viewer_window_manager/presenter.ts +++ b/tools/winscope-ng/src/viewers/viewer_window_manager/presenter.ts @@ -16,29 +16,29 @@ import {TraceType} from "common/trace/trace_type"; import {UiData} from "./ui_data"; -type UiDataCallbackType = (uiData: UiData) => void; +type NotifyViewCallbackType = (uiData: UiData) => void; class Presenter { - constructor(uiDataCallback: UiDataCallbackType) { - this.uiDataCallback = uiDataCallback; + constructor(notifyViewCallback: NotifyViewCallbackType) { + this.notifyViewCallback = notifyViewCallback; this.uiData = new UiData("Initial UI data"); - this.uiDataCallback(this.uiData); + this.notifyViewCallback(this.uiData); } public notifyCurrentTraceEntries(entries: Map) { this.uiData = new UiData("UI data selected by user on time scrub"); - this.uiDataCallback(this.uiData); + this.notifyViewCallback(this.uiData); } public notifyUiEvent() { const oldUiDataText = this.uiData ? this.uiData.text : ""; this.uiData = new UiData(oldUiDataText); this.uiData.text += " | UI data updated because of UI event"; - this.uiDataCallback(this.uiData!); + this.notifyViewCallback(this.uiData!); } - private readonly uiDataCallback: UiDataCallbackType; - private uiData?: UiData; + readonly notifyViewCallback: NotifyViewCallbackType; + uiData?: UiData; } -export {Presenter, UiDataCallbackType}; +export {Presenter}; diff --git a/tools/winscope-ng/src/viewers/viewer_window_manager/ui_data.ts b/tools/winscope-ng/src/viewers/viewer_window_manager/ui_data.ts index 22c97e055..6c36a228a 100644 --- a/tools/winscope-ng/src/viewers/viewer_window_manager/ui_data.ts +++ b/tools/winscope-ng/src/viewers/viewer_window_manager/ui_data.ts @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - class UiData { constructor(public text: string) { + console.log("new UI data", text); } } diff --git a/tools/winscope-ng/src/viewers/viewer_window_manager/viewer_window_manager.component.spec.ts b/tools/winscope-ng/src/viewers/viewer_window_manager/viewer_window_manager.component.spec.ts index 15d8f5b05..ba087b969 100644 --- a/tools/winscope-ng/src/viewers/viewer_window_manager/viewer_window_manager.component.spec.ts +++ b/tools/winscope-ng/src/viewers/viewer_window_manager/viewer_window_manager.component.spec.ts @@ -50,5 +50,4 @@ describe("ViewerWindowManagerComponent", () => { const divInputValue = htmlElement.querySelector(".viewer-window-manager div.input-value"); expect(divInputValue?.innerHTML).toContain("UI Data Value"); }); - }); diff --git a/tools/winscope-ng/src/viewers/viewer_window_manager/viewer_window_manager.component.ts b/tools/winscope-ng/src/viewers/viewer_window_manager/viewer_window_manager.component.ts index 9e2e3270e..00fe4467b 100644 --- a/tools/winscope-ng/src/viewers/viewer_window_manager/viewer_window_manager.component.ts +++ b/tools/winscope-ng/src/viewers/viewer_window_manager/viewer_window_manager.component.ts @@ -22,10 +22,11 @@ import { import {UiData} from "./ui_data"; @Component({ + selector: "viewer-window-manager", template: `
Window Manager
-
Input value: {{inputData.text}}
+
Input value: {{inputData?.text}}
` 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 1f4b1c400..9cfbb06fe 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 @@ -27,6 +27,10 @@ class ViewerWindowManager implements Viewer { this.view.addEventListener("outputEvent", () => this.presenter.notifyUiEvent()); } + public getTitle() { + return "Window Manager"; + } + public notifyCurrentTraceEntries(entries: Map): void { this.presenter.notifyCurrentTraceEntries(entries); } @@ -35,6 +39,10 @@ class ViewerWindowManager implements Viewer { return this.view; } + public getDependencies(): TraceType[] { + return ViewerWindowManager.DEPENDENCIES; + } + public static readonly DEPENDENCIES: TraceType[] = [TraceType.WINDOW_MANAGER]; private view: HTMLElement; private presenter: Presenter;