diff --git a/tools/winscope-ng/package-lock.json b/tools/winscope-ng/package-lock.json index d0f3f4385..cac8d2a03 100644 --- a/tools/winscope-ng/package-lock.json +++ b/tools/winscope-ng/package-lock.json @@ -55,15 +55,16 @@ "@angular/cli": "~14.0.0", "@angular/compiler-cli": "^14.0.0", "@ngxs/devtools-plugin": "^3.7.4", + "@types/chrome": "^0.0.204", "@types/dateformat": "^5.0.0", - "@types/jasmine": "~4.0.0", + "@types/jasmine": "~4.3.1", "@types/jquery": "^3.5.14", "@types/node": "^18.0.4", "@types/w3c-web-usb": "^1.0.6", "@typescript-eslint/eslint-plugin": "^5.30.6", "@typescript-eslint/parser": "^5.30.6", "eslint": "^8.19.0", - "jasmine": "^4.2.1", + "jasmine": "~4.3.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", @@ -3359,6 +3360,16 @@ "@types/node": "*" } }, + "node_modules/@types/chrome": { + "version": "0.0.204", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.204.tgz", + "integrity": "sha512-EvnHfxMHUWP5EAlRMK66uIEUiy36t72vg5RwmzQv9tdIl2ZmAp92NwvmEZJKpbRnIMTEc2BmSmtrFiEISUJ0Sw==", + "dev": true, + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, "node_modules/@types/component-emitter": { "version": "1.2.11", "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", @@ -3462,6 +3473,27 @@ "@types/express": "*" } }, + "node_modules/@types/filesystem": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.32.tgz", + "integrity": "sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==", + "dev": true, + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.29.tgz", + "integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==", + "dev": true + }, + "node_modules/@types/har-format": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.10.tgz", + "integrity": "sha512-o0J30wqycjF5miWDKYKKzzOU1ZTLuA42HZ4HE7/zqTOc/jTLdQ5NhYWvsRQo45Nfi1KHoRdNhteSI4BAxTF1Pg==", + "dev": true + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -3477,9 +3509,9 @@ } }, "node_modules/@types/jasmine": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.0.3.tgz", - "integrity": "sha512-Opp1LvvEuZdk8fSSvchK2mZwhVrsNT0JgJE9Di6MjnaIpmEXM8TLCPPrVtNTYh8+5MPdY8j9bAHMu2SSfwpZJg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.1.tgz", + "integrity": "sha512-Vu8l+UGcshYmV1VWwULgnV/2RDbBaO6i2Ptx7nd//oJPIZGhoI1YLST4VKagD2Pq/Bc2/7zvtvhM7F3p4SN7kQ==", "dev": true }, "node_modules/@types/jquery": { @@ -8976,13 +9008,13 @@ } }, "node_modules/jasmine": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-4.2.1.tgz", - "integrity": "sha512-LNZEKcScnjPRj5J92I1P515bxTvaHMRAERTyCoaGnWr87eOT6zv+b3M+kxKdH/06Gz4TnnXyHbXLiPtMHZ0ncw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-4.3.0.tgz", + "integrity": "sha512-ieBmwkd8L1DXnvSnxx7tecXgA0JDgMXPAwBcqM4lLPedJeI9hTHuWifPynTC+dLe4Y+GkSPSlbqqrmYIgGzYUw==", "dev": true, "dependencies": { "glob": "^7.1.6", - "jasmine-core": "^4.2.0" + "jasmine-core": "^4.3.0" }, "bin": { "jasmine": "bin/jasmine.js" @@ -9025,9 +9057,9 @@ } }, "node_modules/jasmine/node_modules/jasmine-core": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.2.0.tgz", - "integrity": "sha512-OcFpBrIhnbmb9wfI8cqPSJ50pv3Wg4/NSgoZIqHzIwO/2a9qivJWzv8hUvaREIMYYJBas6AvfXATFdVuzzCqVw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.5.0.tgz", + "integrity": "sha512-9PMzyvhtocxb3aXJVOPqBDswdgyAeSB81QnLop4npOpbqnheaTEwPc9ZloQeVswugPManznQBjD8kWDTjlnHuw==", "dev": true }, "node_modules/jasmine/node_modules/minimatch": { @@ -18115,6 +18147,16 @@ "@types/node": "*" } }, + "@types/chrome": { + "version": "0.0.204", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.204.tgz", + "integrity": "sha512-EvnHfxMHUWP5EAlRMK66uIEUiy36t72vg5RwmzQv9tdIl2ZmAp92NwvmEZJKpbRnIMTEc2BmSmtrFiEISUJ0Sw==", + "dev": true, + "requires": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, "@types/component-emitter": { "version": "1.2.11", "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", @@ -18218,6 +18260,27 @@ "@types/express": "*" } }, + "@types/filesystem": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.32.tgz", + "integrity": "sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==", + "dev": true, + "requires": { + "@types/filewriter": "*" + } + }, + "@types/filewriter": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.29.tgz", + "integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==", + "dev": true + }, + "@types/har-format": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.10.tgz", + "integrity": "sha512-o0J30wqycjF5miWDKYKKzzOU1ZTLuA42HZ4HE7/zqTOc/jTLdQ5NhYWvsRQo45Nfi1KHoRdNhteSI4BAxTF1Pg==", + "dev": true + }, "@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -18233,9 +18296,9 @@ } }, "@types/jasmine": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.0.3.tgz", - "integrity": "sha512-Opp1LvvEuZdk8fSSvchK2mZwhVrsNT0JgJE9Di6MjnaIpmEXM8TLCPPrVtNTYh8+5MPdY8j9bAHMu2SSfwpZJg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.1.tgz", + "integrity": "sha512-Vu8l+UGcshYmV1VWwULgnV/2RDbBaO6i2Ptx7nd//oJPIZGhoI1YLST4VKagD2Pq/Bc2/7zvtvhM7F3p4SN7kQ==", "dev": true }, "@types/jquery": { @@ -22309,13 +22372,13 @@ } }, "jasmine": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-4.2.1.tgz", - "integrity": "sha512-LNZEKcScnjPRj5J92I1P515bxTvaHMRAERTyCoaGnWr87eOT6zv+b3M+kxKdH/06Gz4TnnXyHbXLiPtMHZ0ncw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-4.3.0.tgz", + "integrity": "sha512-ieBmwkd8L1DXnvSnxx7tecXgA0JDgMXPAwBcqM4lLPedJeI9hTHuWifPynTC+dLe4Y+GkSPSlbqqrmYIgGzYUw==", "dev": true, "requires": { "glob": "^7.1.6", - "jasmine-core": "^4.2.0" + "jasmine-core": "^4.3.0" }, "dependencies": { "brace-expansion": { @@ -22343,9 +22406,9 @@ } }, "jasmine-core": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.2.0.tgz", - "integrity": "sha512-OcFpBrIhnbmb9wfI8cqPSJ50pv3Wg4/NSgoZIqHzIwO/2a9qivJWzv8hUvaREIMYYJBas6AvfXATFdVuzzCqVw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.5.0.tgz", + "integrity": "sha512-9PMzyvhtocxb3aXJVOPqBDswdgyAeSB81QnLop4npOpbqnheaTEwPc9ZloQeVswugPManznQBjD8kWDTjlnHuw==", "dev": true }, "minimatch": { diff --git a/tools/winscope-ng/package.json b/tools/winscope-ng/package.json index c38fb4655..ac17e55bc 100644 --- a/tools/winscope-ng/package.json +++ b/tools/winscope-ng/package.json @@ -4,12 +4,14 @@ "scripts": { "eslint": "npx eslint --ignore-pattern flickerlib src/", "prettier": "npx prettier --write src/", - "start": "webpack serve --config webpack.config.dev.js --open --hot", + "start": "webpack serve --config webpack.config.dev.js --open --hot --port 8080", + "start:remote_tool_mock": "webpack serve --config src/test/remote_tool_mock/webpack.config.js --open --hot --port 8081", "build:kotlin": "rm -rf kotlin_build && npx kotlinc-js -source-map -source-map-embed-sources always -module-kind commonjs -output kotlin_build/flicker.js ../../../platform_testing/libraries/flicker/src/com/android/server/wm/traces/common", "build:prod": "webpack --config webpack.config.prod.js --progress", + "build:remote_tool_mock": "webpack --config src/test/remote_tool_mock/webpack.config.js --progress", "build:unit": "webpack --config webpack.config.unit.spec.js", "build:e2e": "rm -rf dist/e2e.spec && npx tsc -p ./src/test/e2e", - "build:all": "npm run build:kotlin && npm run build:prod && npm run build:unit && npm run build:e2e", + "build:all": "npm run build:kotlin && npm run build:prod && npm run build:remote_tool_mock && npm run build:unit && npm run build:e2e", "test:unit": "jasmine dist/unit.spec/bundle.js", "test:component": "npx karma start", "test:e2e": "npx protractor protractor.config.js", @@ -64,15 +66,16 @@ "@angular/cli": "~14.0.0", "@angular/compiler-cli": "^14.0.0", "@ngxs/devtools-plugin": "^3.7.4", + "@types/chrome": "^0.0.204", "@types/dateformat": "^5.0.0", - "@types/jasmine": "~4.0.0", + "@types/jasmine": "~4.3.1", "@types/jquery": "^3.5.14", "@types/node": "^18.0.4", "@types/w3c-web-usb": "^1.0.6", "@typescript-eslint/eslint-plugin": "^5.30.6", "@typescript-eslint/parser": "^5.30.6", "eslint": "^8.19.0", - "jasmine": "^4.2.1", + "jasmine": "~4.3.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", diff --git a/tools/winscope-ng/src/abt_chrome_extension/abt_chrome_extension_protocol.ts b/tools/winscope-ng/src/abt_chrome_extension/abt_chrome_extension_protocol.ts new file mode 100644 index 000000000..d33e21518 --- /dev/null +++ b/tools/winscope-ng/src/abt_chrome_extension/abt_chrome_extension_protocol.ts @@ -0,0 +1,91 @@ +/* + * 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 { + AbtChromeExtensionProtocolDependencyInversion, + OnBugAttachmentsReceived +} from "./abt_chrome_extension_protocol_dependency_inversion"; +import { + MessageType, + OpenBuganizerResponse, + OpenRequest, + WebCommandMessage} from "./messages"; +import {FunctionUtils} from "common/utils/function_utils"; + +export class AbtChromeExtensionProtocol implements AbtChromeExtensionProtocolDependencyInversion { + static readonly ABT_EXTENSION_ID = "mbbaofdfoekifkfpgehgffcpagbbjkmj"; + onBugAttachmentsReceived: OnBugAttachmentsReceived = FunctionUtils.DO_NOTHING_ASYNC; + + setOnBugAttachmentsReceived(callback: OnBugAttachmentsReceived) { + this.onBugAttachmentsReceived = callback; + } + + run() { + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get("source") !== "openFromExtension" || !chrome) { + return; + } + + const openRequestMessage: OpenRequest = { + action: MessageType.OPEN_REQUEST + }; + + chrome.runtime.sendMessage( + AbtChromeExtensionProtocol.ABT_EXTENSION_ID, + openRequestMessage, + async (message) => await this.onMessageReceived(message) + ); + } + + private async onMessageReceived(message: WebCommandMessage) { + if (this.isOpenBuganizerResponseMessage(message)) { + await this.onOpenBuganizerResponseMessageReceived(message); + } else { + console.warn("ABT chrome extension protocol received unexpected message:", message); + } + } + + private async onOpenBuganizerResponseMessageReceived(message: OpenBuganizerResponse) { + console.log( + "ABT chrome extension protocol received OpenBuganizerResponse message:", message + ); + + if (message.attachments.length === 0) { + console.warn("ABT chrome extension protocol received no attachments"); + return; + } + + const filesBlobPromises = message.attachments.map(async attachment => { + const fileQueryResponse = await fetch(attachment.objectUrl); + const blob = await fileQueryResponse.blob(); + + // Note: the received blob's media type is wrong. It is always set to "image/png". + // Context: http://google3/javascript/closure/html/safeurl.js?g=0&l=256&rcl=273756987 + // Cloning the blob clears the media type. + const file = new File([blob], attachment.name); + + return file; + }); + + const files = await Promise.all(filesBlobPromises); + await this.onBugAttachmentsReceived(files); + } + + private isOpenBuganizerResponseMessage(message: WebCommandMessage): + message is OpenBuganizerResponse { + return message.action === MessageType.OPEN_BUGANIZER_RESPONSE; + } +} diff --git a/tools/winscope-ng/src/abt_chrome_extension/abt_chrome_extension_protocol_dependency_inversion.ts b/tools/winscope-ng/src/abt_chrome_extension/abt_chrome_extension_protocol_dependency_inversion.ts new file mode 100644 index 000000000..4d1d363b3 --- /dev/null +++ b/tools/winscope-ng/src/abt_chrome_extension/abt_chrome_extension_protocol_dependency_inversion.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ + +export type OnBugAttachmentsReceived = (attachments: File[]) => Promise; + +export interface AbtChromeExtensionProtocolDependencyInversion { + setOnBugAttachmentsReceived(callback: OnBugAttachmentsReceived): void; + run(): void; +} diff --git a/tools/winscope-ng/src/abt_chrome_extension/abt_chrome_extension_protocol_stub.ts b/tools/winscope-ng/src/abt_chrome_extension/abt_chrome_extension_protocol_stub.ts new file mode 100644 index 000000000..9f60b7f50 --- /dev/null +++ b/tools/winscope-ng/src/abt_chrome_extension/abt_chrome_extension_protocol_stub.ts @@ -0,0 +1,30 @@ +/* + * 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 { + OnBugAttachmentsReceived, + AbtChromeExtensionProtocolDependencyInversion +} from "./abt_chrome_extension_protocol_dependency_inversion"; + +export class AbtChromeExtensionProtocolStub implements AbtChromeExtensionProtocolDependencyInversion { + setOnBugAttachmentsReceived(callback: OnBugAttachmentsReceived) { + // do nothing + } + + run() { + // do nothing + } +} diff --git a/tools/winscope-ng/src/abt_chrome_extension/messages.ts b/tools/winscope-ng/src/abt_chrome_extension/messages.ts new file mode 100644 index 000000000..d6ad07da8 --- /dev/null +++ b/tools/winscope-ng/src/abt_chrome_extension/messages.ts @@ -0,0 +1,133 @@ +/* + * 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. + */ + +// TODO (b/262667224): +// deduplicate all the type definitions in this file when we move Winscope to google3. +// The definitions were duplicated from these source files: +// - google3/wireless/android/tools/android_bug_tool/app/platform/web/interface/command.ts +// - google3/wireless/android/tools/android_bug_tool/app/platform/web/interface/attachment_metadata.ts +// - google3/wireless/android/tools/android_bug_tool/app/platform/web/interface/bug_report_metadata.ts + +/** Describes the type of message enclosed in a {@code WebCommandMessage}. */ +export enum MessageType { + UNKNOWN, + OPEN_REQUEST, + OPEN_BUGANIZER_RESPONSE, + OPEN_WEB_RESPONSE, + OPEN_URL_REQUEST, + OPEN_URL_RESPONSE, + CHECK_ISSUE_METADATA_REQUEST, + CHECK_ISSUE_METADATA_RESPONSE, + OPEN_TOOL_WEB_REQUEST, +} + +/** Base of all messages sent between the web and extension. */ +export declare interface WebCommandMessage { + action: MessageType; +} + +/** Request from web to background to download the file. */ +export interface OpenRequest extends WebCommandMessage { + action: MessageType.OPEN_REQUEST; +} + +/** Response of download the issue's attachment from background. */ +export declare interface OpenBuganizerResponse extends WebCommandMessage { + action: MessageType.OPEN_BUGANIZER_RESPONSE; + + /** issue id */ + issueId: string; + + /** issue title */ + issueTitle: string|undefined; + + /** issue access level */ + issueAccessLevel: IssueAccessLimit|undefined; + + /** Attachment list. */ + attachments: AttachmentMetadata[]; +} + +/** Attachment metadata. */ +export interface AttachmentMetadata { + bugReportMetadata?: BugReportMetadata; + author: string; + name: string; + objectUrl: string; + restrictionSeverity: RestrictionSeverity; + resourceName: string; + entityStatus: EntityStatus; + attachmentId: string; + fileSize: number; + commentTimestamp?: DateTime; +} + +/** + * Incorporates all of the metadata that can be retrieved from a bugreport + * file name. + */ +export interface BugReportMetadata { + uuid?: string; + hasWinscope: boolean; + hasTrace: boolean; + isRedacted: boolean; + device: string; + build: string; + // The date parsed from the bug report filename is only used for + // grouping common files together. It is not used for display purposes. + timestamp: DateTime; +} + +/** + * Defines of the issue access limit. See: + * http://go/buganizer/concepts/access-control#accesslimit + */ +export const enum IssueAccessLimit { + INTERNAL = "", + VISIBLE_TO_PARTNERS = "Visible to Partners", + VISIBLE_TO_PUBLIC = "Visible to Public", +} + +/** + * Types of issue content restriction verdicts. See: + * http://google3/google/devtools/issuetracker/v1/issuetracker.proto?l=1858&rcl=278024740 + */ +export enum RestrictionSeverity { + /** Unspecified restricted content severity */ + RESTRICTION_SEVERITY_UNSPECIFIED = 0, + /** No restricted content was detected/flagged in the content */ + NONE_DETECTED = 1, + /** Restricted content was detected/flagged in the content */ + RESTRICTED = 2, + /** RESTRICTED_PLUS content was detected/flagged in the content */ + RESTRICTED_PLUS = 3, +} + +/** + * Types of entity statuses for issue tracker attachments. See: + * https:google3/google/devtools/issuetracker/v1/issuetracker.proto;rcl=448855448;l=58 + */ +export enum EntityStatus { + // Default value. Entity exists and is available for use. + ACTIVE = 0, + // Entity is invisible except for administrative actions, i.e. undelete. + DELETED = 1, + // Entity is irretrievably wiped. + PURGED = 2, +} + +// Actual definition is in google3/third_party/javascript/closure/date/date +export type DateTime = object; diff --git a/tools/winscope-ng/src/app/components/app.component.ts b/tools/winscope-ng/src/app/components/app.component.ts index 826f12106..bd407479f 100644 --- a/tools/winscope-ng/src/app/components/app.component.ts +++ b/tools/winscope-ng/src/app/components/app.component.ts @@ -23,13 +23,15 @@ import { ViewEncapsulation } from "@angular/core"; import { createCustomElement } from "@angular/elements"; +import { AppComponentDependencyInversion } from "./app_component_dependency_inversion"; import { TimelineComponent} from "./timeline/timeline.component"; +import {AbtChromeExtensionProtocol} from "abt_chrome_extension/abt_chrome_extension_protocol"; +import {CrossToolProtocol} from "cross_tool/cross_tool_protocol"; import { Mediator } from "app/mediator"; import { TraceData } from "app/trace_data"; import { PersistentStore } from "common/utils/persistent_store"; import { Timestamp } from "common/trace/timestamp"; import { FileUtils } from "common/utils/file_utils"; -import { FunctionUtils } from "common/utils/function_utils"; import { proxyClient, ProxyState } from "trace_collection/proxy_client"; import { ViewerInputMethodComponent } from "viewers/components/viewer_input_method.component"; import { View, Viewer } from "viewers/viewer"; @@ -124,14 +126,14 @@ import {TRACE_INFO} from "app/trace_info"; @@ -164,9 +166,6 @@ import {TRACE_INFO} from "app/trace_info"; flex-direction: column; overflow: auto; } - .timescrub { - margin: 8px; - } .center { display: flex; align-content: center; @@ -188,12 +187,21 @@ import {TRACE_INFO} from "app/trace_info"; ], encapsulation: ViewEncapsulation.None }) -export class AppComponent { +export class AppComponent implements AppComponentDependencyInversion { title = "winscope-ng"; changeDetectorRef: ChangeDetectorRef; traceData = new TraceData(); timelineData = new TimelineData(); - mediator = new Mediator(this.traceData, this.timelineData); + abtChromeExtensionProtocol = new AbtChromeExtensionProtocol(); + crossToolProtocol = new CrossToolProtocol(); + mediator = new Mediator( + this.traceData, + this.timelineData, + this.abtChromeExtensionProtocol, + this.crossToolProtocol, + this, + localStorage + ); states = ProxyState; store: PersistentStore = new PersistentStore(); currentTimestamp?: Timestamp; @@ -257,24 +265,23 @@ export class AppComponent { public onUploadNewClick() { this.dataLoaded = false; - this.mediator.clearData(); + this.mediator.onWinscopeUploadNew(); proxyClient.adbData = []; this.changeDetectorRef.detectChanges(); } + public onTraceDataLoaded(viewers: Viewer[]) { + this.viewers = viewers; + this.dataLoaded = true; + this.changeDetectorRef.detectChanges(); + } + public setDarkMode(enabled: boolean) { document.body.classList.toggle("dark-mode", enabled); this.store.add("dark-mode", `${enabled}`); this.isDarkModeOn = enabled; } - public onTraceDataLoaded() { - this.mediator.onTraceDataLoaded(localStorage); - this.viewers = this.mediator.getViewers(); - this.dataLoaded = true; - this.changeDetectorRef.detectChanges(); - } - async onDownloadTracesButtonClick() { const traceFiles = await this.makeTraceFilesForDownload(); const zipFileBlob = await FileUtils.createZipArchive(traceFiles); diff --git a/tools/winscope-ng/src/app/components/app_component_dependency_inversion.ts b/tools/winscope-ng/src/app/components/app_component_dependency_inversion.ts new file mode 100644 index 000000000..eacb62bd7 --- /dev/null +++ b/tools/winscope-ng/src/app/components/app_component_dependency_inversion.ts @@ -0,0 +1,21 @@ +/* + * 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 {Viewer} from "viewers/viewer"; + +export interface AppComponentDependencyInversion { + onTraceDataLoaded(viewers: Viewer[]): void; +} diff --git a/tools/winscope-ng/src/app/components/app_component_stub.ts b/tools/winscope-ng/src/app/components/app_component_stub.ts new file mode 100644 index 000000000..554ef7cd0 --- /dev/null +++ b/tools/winscope-ng/src/app/components/app_component_stub.ts @@ -0,0 +1,24 @@ +/* + * 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 {AppComponentDependencyInversion} from "./app_component_dependency_inversion"; +import {Viewer} from "viewers/viewer"; + +export class AppComponentStub implements AppComponentDependencyInversion { + onTraceDataLoaded(viewers: Viewer[]) { + // do nothing + } +} diff --git a/tools/winscope-ng/src/app/components/timeline/timeline.component.spec.ts b/tools/winscope-ng/src/app/components/timeline/timeline.component.spec.ts index 5c2804fba..ff8b36916 100644 --- a/tools/winscope-ng/src/app/components/timeline/timeline.component.spec.ts +++ b/tools/winscope-ng/src/app/components/timeline/timeline.component.spec.ts @@ -34,6 +34,10 @@ import {MatInputModule} from "@angular/material/input"; import { SingleTimelineComponent } from "./single_timeline.component"; import {Mediator} from "app/mediator"; import {TraceData} from "app/trace_data"; +import {AbtChromeExtensionProtocolStub} from "abt_chrome_extension/abt_chrome_extension_protocol_stub"; +import {CrossToolProtocolStub} from "cross_tool/cross_tool_protocol_stub"; +import {AppComponentStub} from "app/components/app_component_stub"; +import {MockStorage} from "test/unit/mock_storage"; describe("TimelineComponent", () => { let fixture: ComponentFixture; @@ -70,7 +74,17 @@ describe("TimelineComponent", () => { const traceData = new TraceData(); const timelineData = new TimelineData(); - component.mediator = new Mediator(traceData, timelineData); + const abtChromeExtensionProtocol = new AbtChromeExtensionProtocolStub(); + const crossToolProtocol = new CrossToolProtocolStub(); + const appComponent = new AppComponentStub(); + component.mediator = new Mediator( + traceData, + timelineData, + abtChromeExtensionProtocol, + crossToolProtocol, + appComponent, + new MockStorage() + ); component.timelineData = timelineData; }); diff --git a/tools/winscope-ng/src/app/components/timeline/timeline.component.ts b/tools/winscope-ng/src/app/components/timeline/timeline.component.ts index 09b1cf2c5..6fcbd82bf 100644 --- a/tools/winscope-ng/src/app/components/timeline/timeline.component.ts +++ b/tools/winscope-ng/src/app/components/timeline/timeline.component.ts @@ -31,10 +31,10 @@ import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; import { TraceType } from "common/trace/trace_type"; import { TRACE_INFO } from "app/trace_info"; import { Mediator } from "app/mediator"; +import { TimelineComponentDependencyInversion } from "./timeline_component_dependency_inversion"; import { TimelineData } from "app/timeline_data"; import { MiniTimelineComponent } from "./mini_timeline.component"; import { ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType } from "common/trace/timestamp"; -import { FunctionUtils } from "common/utils/function_utils"; import { TimeUtils } from "common/utils/time_utils"; @Component({ @@ -272,7 +272,7 @@ import { TimeUtils } from "common/utils/time_utils"; } `], }) -export class TimelineComponent { +export class TimelineComponent implements TimelineComponentDependencyInversion { public readonly TOGGLE_BUTTON_CLASS: string = "button-toggle-expansion"; public readonly MAX_SELECTED_TRACES = 3; @@ -339,9 +339,7 @@ export class TimelineComponent { } ngOnInit() { - this.mediator.setNotifyCurrentTimestampChangedToTimelineComponentCallback((timestamp: Timestamp|undefined) => { - this.onCurrentTimestampChanged(timestamp); - }); + this.mediator.setTimelineComponent(this); if (this.timelineData.hasTimestamps()) { this.updateTimeInputValuesToCurrentTimestamp(); @@ -355,9 +353,7 @@ export class TimelineComponent { } ngOnDestroy() { - this.mediator.setNotifyCurrentTimestampChangedToTimelineComponentCallback( - FunctionUtils.DO_NOTHING - ); + this.mediator.setTimelineComponent(undefined); } ngAfterViewInit() { diff --git a/tools/winscope-ng/src/app/components/timeline/timeline_component_dependency_inversion.ts b/tools/winscope-ng/src/app/components/timeline/timeline_component_dependency_inversion.ts new file mode 100644 index 000000000..90fecd60f --- /dev/null +++ b/tools/winscope-ng/src/app/components/timeline/timeline_component_dependency_inversion.ts @@ -0,0 +1,21 @@ +/* + * 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 {Timestamp} from "common/trace/timestamp"; + +export interface TimelineComponentDependencyInversion { + onCurrentTimestampChanged(timestamp: Timestamp|undefined): void; +} diff --git a/tools/winscope-ng/src/app/components/timeline/timeline_component_stub.ts b/tools/winscope-ng/src/app/components/timeline/timeline_component_stub.ts new file mode 100644 index 000000000..bdba64a1a --- /dev/null +++ b/tools/winscope-ng/src/app/components/timeline/timeline_component_stub.ts @@ -0,0 +1,24 @@ +/* + * 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 {TimelineComponentDependencyInversion} from "./timeline_component_dependency_inversion"; +import {Timestamp} from "common/trace/timestamp"; + +export class TimelineComponentStub implements TimelineComponentDependencyInversion { + onCurrentTimestampChanged(timestamp: Timestamp|undefined) { + // do nothing + } +} diff --git a/tools/winscope-ng/src/app/mediator.spec.ts b/tools/winscope-ng/src/app/mediator.spec.ts index 88dfe236d..2e1c5d3d4 100644 --- a/tools/winscope-ng/src/app/mediator.spec.ts +++ b/tools/winscope-ng/src/app/mediator.spec.ts @@ -13,8 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {Timestamp, TimestampType} from "common/trace/timestamp"; + +import {AppComponentStub} from "./components/app_component_stub"; +import {TimelineComponentStub} from "./components/timeline/timeline_component_stub"; import {Mediator} from "./mediator"; +import {AbtChromeExtensionProtocolStub} from "abt_chrome_extension/abt_chrome_extension_protocol_stub"; +import {CrossToolProtocolStub} from "cross_tool/cross_tool_protocol_stub"; +import {RealTimestamp} from "common/trace/timestamp"; import {UnitTestUtils} from "test/unit/utils"; import {ViewerFactory} from "viewers/viewer_factory"; import {ViewerStub} from "viewers/viewer_stub"; @@ -22,82 +27,137 @@ import {TimelineData} from "./timeline_data"; import {TraceData} from "./trace_data"; import {MockStorage} from "test/unit/mock_storage"; -class TimelineComponentStub { - onCurrentTimestampChanged(timestamp: Timestamp|undefined) { - // do nothing - } -} - describe("Mediator", () => { const viewerStub = new ViewerStub("Title"); - let timelineComponent: TimelineComponentStub; let traceData: TraceData; let timelineData: TimelineData; + let abtChromeExtensionProtocol: AbtChromeExtensionProtocolStub; + let crossToolProtocol: CrossToolProtocolStub; + let appComponent: AppComponentStub; + let timelineComponent: TimelineComponentStub; let mediator: Mediator; beforeEach(async () => { timelineComponent = new TimelineComponentStub(); traceData = new TraceData(); timelineData = new TimelineData(); - mediator = new Mediator(traceData, timelineData); - mediator.setNotifyCurrentTimestampChangedToTimelineComponentCallback(timestamp => { - timelineComponent.onCurrentTimestampChanged(timestamp); - }); + abtChromeExtensionProtocol = new AbtChromeExtensionProtocolStub(); + crossToolProtocol = new CrossToolProtocolStub(); + appComponent = new AppComponentStub(); + timelineComponent = new TimelineComponentStub(); + mediator = new Mediator( + traceData, + timelineData, + abtChromeExtensionProtocol, + crossToolProtocol, + appComponent, + new MockStorage() + ); + mediator.setTimelineComponent(timelineComponent); spyOn(ViewerFactory.prototype, "createViewers").and.returnValue([viewerStub]); }); - it("initializes TimelineData on data load event", async () => { + it("handles data load event from Winscope", async () => { spyOn(timelineData, "initialize").and.callThrough(); - - await loadTraces(); - expect(timelineData.initialize).toHaveBeenCalledTimes(0); - - mediator.onTraceDataLoaded(new MockStorage()); - expect(timelineData.initialize).toHaveBeenCalledTimes(1); - }); - - - it("it creates viewers on data load event", async () => { + spyOn(appComponent, "onTraceDataLoaded"); spyOn(viewerStub, "notifyCurrentTraceEntries"); await loadTraces(); - expect(mediator.getViewers()).toEqual([]); - - mediator.onTraceDataLoaded(new MockStorage()); - expect(mediator.getViewers()).toEqual([viewerStub]); + expect(timelineData.initialize).toHaveBeenCalledTimes(0); + expect(appComponent.onTraceDataLoaded).toHaveBeenCalledTimes(0); + expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(0); + mediator.onWinscopeTraceDataLoaded(); + expect(timelineData.initialize).toHaveBeenCalledTimes(1); + expect(appComponent.onTraceDataLoaded).toHaveBeenCalledOnceWith([viewerStub]); // notifies viewer about current timestamp on creation expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(1); }); - it("forwards timestamp changed events/notifications", async () => { - const timestamp10 = new Timestamp(TimestampType.REAL, 10n); - const timestamp11 = new Timestamp(TimestampType.REAL, 11n); + + //TODO: enable/adapt this test once FileUtils is fully compatible with Node.js (b/262269229). + // FileUtils#unzipFile() currently can't execute on Node.js. + //it("processes bugreport message from remote tool", async () => { + // spyOn(traceData, "loadTraces").and.callThrough(); + // spyOn(timelineData, "initialize").and.callThrough(); + // spyOn(appComponent, "onTraceDataLoaded"); + // spyOn(viewerStub, "notifyCurrentTraceEntries"); + + // const bugreport = await UnitTestUtils.getFixtureFile("bugreports/bugreport_stripped.zip"); + // const timestamp = new RealTimestamp(10n); + // await crossToolProtocol.onBugreportReceived(bugreport, timestamp); + + // expect(traceData.loadTraces).toHaveBeenCalledOnceWith([bugreport]); + // expect(timelineData.initialize).toHaveBeenCalledTimes(1); + // expect(appComponent.onTraceDataLoaded).toHaveBeenCalledOnceWith([viewerStub]); + // expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(1); + //}); + + it("propagates current timestamp changed through timeline", async () => { + const timestamp10 = new RealTimestamp(10n); + const timestamp11 = new RealTimestamp(11n); await loadTraces(); - mediator.onTraceDataLoaded(new MockStorage()); - expect(mediator.getViewers()).toEqual([viewerStub]); + mediator.onWinscopeTraceDataLoaded(); spyOn(viewerStub, "notifyCurrentTraceEntries"); spyOn(timelineComponent, "onCurrentTimestampChanged"); + spyOn(crossToolProtocol, "sendTimestamp"); expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(0); expect(timelineComponent.onCurrentTimestampChanged).toHaveBeenCalledTimes(0); + expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(0); // notify timestamp timelineData.setCurrentTimestamp(timestamp10); expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(1); expect(timelineComponent.onCurrentTimestampChanged).toHaveBeenCalledTimes(1); + expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(1); - // notify timestamp again (no timestamp change) + // notify same timestamp again (ignored, no timestamp change) timelineData.setCurrentTimestamp(timestamp10); expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(1); expect(timelineComponent.onCurrentTimestampChanged).toHaveBeenCalledTimes(1); + expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(1); - // reset back to the default timestamp should trigger a change + // notify another timestamp timelineData.setCurrentTimestamp(timestamp11); expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(2); expect(timelineComponent.onCurrentTimestampChanged).toHaveBeenCalledTimes(2); + expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(2); + }); + + it("propagates timestamp received from remote tool", async () => { + const timestamp10 = new RealTimestamp(10n); + const timestamp11 = new RealTimestamp(11n); + + await loadTraces(); + mediator.onWinscopeTraceDataLoaded(); + + spyOn(viewerStub, "notifyCurrentTraceEntries"); + spyOn(timelineComponent, "onCurrentTimestampChanged"); + spyOn(crossToolProtocol, "sendTimestamp"); + expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(0); + expect(timelineComponent.onCurrentTimestampChanged).toHaveBeenCalledTimes(0); + expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(0); + + // receive timestamp + await crossToolProtocol.onTimestampReceived(timestamp10); + expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(1); + expect(timelineComponent.onCurrentTimestampChanged).toHaveBeenCalledTimes(1); + expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(0); + + // receive same timestamp again (ignored, no timestamp change) + await crossToolProtocol.onTimestampReceived(timestamp10); + expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(1); + expect(timelineComponent.onCurrentTimestampChanged).toHaveBeenCalledTimes(1); + expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(0); + + // receive another + await crossToolProtocol.onTimestampReceived(timestamp11); + expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(2); + expect(timelineComponent.onCurrentTimestampChanged).toHaveBeenCalledTimes(2); + expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(0); }); const loadTraces = async () => { diff --git a/tools/winscope-ng/src/app/mediator.ts b/tools/winscope-ng/src/app/mediator.ts index 3d0d92a04..d2e5afadc 100644 --- a/tools/winscope-ng/src/app/mediator.ts +++ b/tools/winscope-ng/src/app/mediator.ts @@ -14,71 +14,200 @@ * limitations under the License. */ -import {Timestamp} from "common/trace/timestamp"; -import {TraceType} from "common/trace/trace_type"; -import {FunctionUtils} from "common/utils/function_utils"; -import { Viewer } from "viewers/viewer"; -import { ViewerFactory } from "viewers/viewer_factory"; +import {AppComponentDependencyInversion} from "./components/app_component_dependency_inversion"; +import {TimelineComponentDependencyInversion} + from "./components/timeline/timeline_component_dependency_inversion"; import {TimelineData} from "./timeline_data"; import {TraceData} from "./trace_data"; +import {AbtChromeExtensionProtocolDependencyInversion} + from "abt_chrome_extension/abt_chrome_extension_protocol_dependency_inversion"; +import {CrossToolProtocolDependencyInversion} + from "cross_tool/cross_tool_protocol_dependency_inversion"; +import {FileUtils} from "common/utils/file_utils"; +import {Timestamp, TimestampType} from "common/trace/timestamp"; +import {TraceType} from "common/trace/trace_type"; +import {Viewer} from "viewers/viewer"; +import {ViewerFactory} from "viewers/viewer_factory"; -type CurrentTimestampChangedCallback = (timestamp: Timestamp|undefined) => void; - -class Mediator { +export class Mediator { private traceData: TraceData; private timelineData: TimelineData; + private abtChromeExtensionProtocol: AbtChromeExtensionProtocolDependencyInversion; + private crossToolProtocol: CrossToolProtocolDependencyInversion; + private appComponent: AppComponentDependencyInversion; + private timelineComponent?: TimelineComponentDependencyInversion; + private storage: Storage; private viewers: Viewer[] = []; - private notifyCurrentTimestampChangedToTimelineComponent: CurrentTimestampChangedCallback = - FunctionUtils.DO_NOTHING; + private isChangingCurrentTimestamp = false; + private blockWhileRemoteToolBugreportIsBeingLoaded = Promise.resolve(); + + constructor( + traceData: TraceData, + timelineData: TimelineData, + abtChromeExtensionProtocol: AbtChromeExtensionProtocolDependencyInversion, + crossToolProtocol: CrossToolProtocolDependencyInversion, + appComponent: AppComponentDependencyInversion, + storage: Storage) { - constructor(traceData: TraceData, timelineData: TimelineData) { this.traceData = traceData; this.timelineData = timelineData; - this.timelineData.setOnCurrentTimestampChangedCallback(timestamp => { - this.onCurrentTimestampChanged(timestamp); + this.abtChromeExtensionProtocol = abtChromeExtensionProtocol; + this.crossToolProtocol = crossToolProtocol; + this.appComponent = appComponent; + this.storage = storage; + + this.timelineData.setOnCurrentTimestampChanged(timestamp => { + this.onWinscopeCurrentTimestampChanged(timestamp); + }); + + this.crossToolProtocol.setOnBugreportReceived(async (bugreport: File, timestamp?: Timestamp) => { + await this.onRemoteToolBugreportReceived(bugreport, timestamp); + }); + + this.crossToolProtocol.setOnTimestampReceived(async (timestamp: Timestamp) => { + await this.onRemoteToolTimestampReceived(timestamp); + }); + + this.abtChromeExtensionProtocol.setOnBugAttachmentsReceived(async (attachments: File[]) => { + await this.onAbtChromeExtensionBugAttachmentsReceived(attachments); + }); + this.abtChromeExtensionProtocol.run(); + } + + public setTimelineComponent(timelineComponent: TimelineComponentDependencyInversion|undefined) { + this.timelineComponent = timelineComponent; + } + + public onWinscopeTraceDataLoaded() { + this.processTraceData(); + } + + public async onRemoteToolBugreportReceived(bugreport: File, timestamp?: Timestamp) { + let unblockOtherRemoteToolEventHandlers: () => void; + + this.blockWhileRemoteToolBugreportIsBeingLoaded = new Promise(resolve => { + unblockOtherRemoteToolEventHandlers = resolve; + }); + + try { + await this.processFiles([bugreport]); + } finally { + unblockOtherRemoteToolEventHandlers!(); + } + + if (timestamp !== undefined) { + await this.onRemoteToolTimestampReceived(timestamp); + } + } + + public async onAbtChromeExtensionBugAttachmentsReceived(attachments: File[]) { + await this.processFiles(attachments); + } + + public onWinscopeCurrentTimestampChanged(timestamp: Timestamp|undefined) { + this.executeIgnoringRecursiveTimestampNotifications(() => { + const entries = this.traceData.getTraceEntries(timestamp); + this.viewers.forEach(viewer => { + viewer.notifyCurrentTraceEntries(entries); + }); + + if (timestamp) { + if (timestamp.getType() !== TimestampType.REAL) { + console.warn( + "Cannot propagate timestamp change to remote tool." + + ` Remote tool expects timestamp type ${TimestampType.REAL},` + + ` but Winscope wants to notify timestamp type ${timestamp.getType()}.` + ); + } else { + this.crossToolProtocol.sendTimestamp(timestamp); + } + } + + this.timelineComponent?.onCurrentTimestampChanged(timestamp); }); } - public setNotifyCurrentTimestampChangedToTimelineComponentCallback(callback: CurrentTimestampChangedCallback) { - this.notifyCurrentTimestampChangedToTimelineComponent = callback; - } + public async onRemoteToolTimestampReceived(timestamp: Timestamp) { + await this.executeIgnoringRecursiveTimestampNotificationsAsync(async () => { + if (this.timelineData.getTimestampType() != TimestampType.REAL) { + console.warn( + "Cannot apply new timestamp received from remote tool." + + ` Remote tool notified timestamp type type ${TimestampType.REAL},` + + ` but Winscope is accepting timestamp type ${this.timelineData.getTimestampType()}.` + ); + } - public getViewers(): Viewer[] { - return this.viewers; - } + if (this.timelineData.getCurrentTimestamp() === timestamp) { + return; // no timestamp change + } - public onTraceDataLoaded(storage: Storage) { - this.timelineData.initialize( - this.traceData.getTimelines(), - this.traceData.getScreenRecordingVideo() - ); - this.createViewers(storage); - } + // Make sure we finished loading the bugreport, before notifying the timestamp to the rest of + // the system. Otherwise, the timestamp notification would just get lost. + await this.blockWhileRemoteToolBugreportIsBeingLoaded; - public onCurrentTimestampChanged(timestamp: Timestamp|undefined) { - const entries = this.traceData.getTraceEntries(timestamp); - this.viewers.forEach(viewer => { - viewer.notifyCurrentTraceEntries(entries); + const entries = this.traceData.getTraceEntries(timestamp); + this.viewers.forEach(viewer => { + viewer.notifyCurrentTraceEntries(entries); + }); + + this.timelineData.setCurrentTimestamp(timestamp); + this.timelineComponent?.onCurrentTimestampChanged(timestamp); }); - - this.notifyCurrentTimestampChangedToTimelineComponent(timestamp); } - public clearData() { + public onWinscopeUploadNew() { this.traceData.clear(); this.timelineData.clear(); this.viewers = []; } - private createViewers(storage: Storage) { + private async processFiles(files: File[]) { + const unzippedFiles = await FileUtils.unzipFilesIfNeeded(files); + this.traceData.clear(); + await this.traceData.loadTraces(unzippedFiles); + this.processTraceData(); + } + + private processTraceData() { + this.timelineData.initialize( + this.traceData.getTimelines(), + this.traceData.getScreenRecordingVideo() + ); + this.createViewers(); + this.appComponent.onTraceDataLoaded(this.viewers); + } + + private createViewers() { const traceTypes = this.traceData.getLoadedTraces().map(trace => trace.type); - this.viewers = new ViewerFactory().createViewers(new Set(traceTypes), storage); + this.viewers = new ViewerFactory().createViewers(new Set(traceTypes), this.storage); // Make sure to update the viewers active entries as soon as they are created. if (this.timelineData.getCurrentTimestamp()) { - this.onCurrentTimestampChanged(this.timelineData.getCurrentTimestamp()); + this.onWinscopeCurrentTimestampChanged(this.timelineData.getCurrentTimestamp()); + } + } + + private executeIgnoringRecursiveTimestampNotifications(op: () => void) { + if (this.isChangingCurrentTimestamp) { + return; + } + this.isChangingCurrentTimestamp = true; + try { + op(); + } finally { + this.isChangingCurrentTimestamp = false; + } + } + + private async executeIgnoringRecursiveTimestampNotificationsAsync(op: () => Promise) { + if (this.isChangingCurrentTimestamp) { + return; + } + this.isChangingCurrentTimestamp = true; + try { + await op(); + } finally { + this.isChangingCurrentTimestamp = false; } } } - -export { Mediator }; diff --git a/tools/winscope-ng/src/app/timeline_data.spec.ts b/tools/winscope-ng/src/app/timeline_data.spec.ts index 0543f41fd..d920e6327 100644 --- a/tools/winscope-ng/src/app/timeline_data.spec.ts +++ b/tools/winscope-ng/src/app/timeline_data.spec.ts @@ -43,7 +43,7 @@ describe("TimelineData", () => { beforeEach(() => { timelineData = new TimelineData(); - timelineData.setOnCurrentTimestampChangedCallback(timestamp => { + timelineData.setOnCurrentTimestampChanged(timestamp => { timestampChangedObserver.onCurrentTimestampChanged(timestamp); }); }); diff --git a/tools/winscope-ng/src/app/timeline_data.ts b/tools/winscope-ng/src/app/timeline_data.ts index eb713318a..b2c9b49e8 100644 --- a/tools/winscope-ng/src/app/timeline_data.ts +++ b/tools/winscope-ng/src/app/timeline_data.ts @@ -56,7 +56,7 @@ export class TimelineData { this.onCurrentTimestampChanged(this.getCurrentTimestamp()); } - setOnCurrentTimestampChangedCallback(callback: TimestampCallbackType) { + setOnCurrentTimestampChanged(callback: TimestampCallbackType) { this.onCurrentTimestampChanged = callback; } diff --git a/tools/winscope-ng/src/common/utils/function_utils.ts b/tools/winscope-ng/src/common/utils/function_utils.ts index 6e52d130b..c5c2b3f9f 100644 --- a/tools/winscope-ng/src/common/utils/function_utils.ts +++ b/tools/winscope-ng/src/common/utils/function_utils.ts @@ -20,4 +20,8 @@ export class FunctionUtils { static readonly DO_NOTHING = () => { // do nothing }; + + static readonly DO_NOTHING_ASYNC = (): Promise => { + return Promise.resolve(); + }; } diff --git a/tools/winscope-ng/src/common/utils/global_config.ts b/tools/winscope-ng/src/common/utils/global_config.ts new file mode 100644 index 000000000..eaff74462 --- /dev/null +++ b/tools/winscope-ng/src/common/utils/global_config.ts @@ -0,0 +1,31 @@ +/* + * 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. + */ + +export interface Schema { + MODE: "DEV"|"PROD"; + REMOTE_TOOL_URL: "http://localhost:8081"|"https://android-build.googleplex.com/builds/bug_tool"; +} + +export class GlobalConfig implements Schema { + readonly MODE = "PROD" as const; + readonly REMOTE_TOOL_URL = "https://android-build.googleplex.com/builds/bug_tool" as const; + + set(config: Schema) { + Object.assign(this, config); + } +} + +export const globalConfig = new GlobalConfig(); diff --git a/tools/winscope-ng/src/cross_tool/cross_tool_protocol.ts b/tools/winscope-ng/src/cross_tool/cross_tool_protocol.ts new file mode 100644 index 000000000..c404f5733 --- /dev/null +++ b/tools/winscope-ng/src/cross_tool/cross_tool_protocol.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 {Message, MessageBugReport, MessagePong, MessageTimestamp, MessageType} from "./messages"; +import { + CrossToolProtocolDependencyInversion, + OnBugreportReceived, + OnTimestampReceived} from "cross_tool/cross_tool_protocol_dependency_inversion"; +import {RealTimestamp} from "common/trace/timestamp"; +import {FunctionUtils} from "common/utils/function_utils"; +import {globalConfig} from "common/utils/global_config"; + +class CrossToolProtocol implements CrossToolProtocolDependencyInversion { + private remoteToolWindow?: Window; + private onBugreportReceived: OnBugreportReceived = FunctionUtils.DO_NOTHING_ASYNC; + private onTimestampReceived: OnTimestampReceived = FunctionUtils.DO_NOTHING_ASYNC; + + constructor() { + window.addEventListener("message", async (event) => { + await this.onMessageReceived(event); + }); + } + + setOnBugreportReceived(callback: OnBugreportReceived) { + this.onBugreportReceived = callback; + } + + setOnTimestampReceived(callback: OnTimestampReceived) { + this.onTimestampReceived = callback; + } + + sendTimestamp(timestamp: RealTimestamp) { + if (!this.remoteToolWindow) { + return; + } + + const message = new MessageTimestamp(timestamp.getValueNs()); + this.remoteToolWindow.postMessage(message, globalConfig.REMOTE_TOOL_URL); + console.log("Cross-tool protocol sent timestamp message:", message); + } + + private async onMessageReceived(event: MessageEvent) { + if (event.origin !== globalConfig.REMOTE_TOOL_URL) { + console.log("Cross-tool protocol ignoring message from unexpected origin.", + "Origin:", event.origin, "Message:", event.data); + return; + } + + this.remoteToolWindow = event.source as Window; + + const message = event.data as Message; + if (!message.type) { + console.log("Cross-tool protocol received invalid message:", message); + return; + } + + switch(message.type) { + case MessageType.PING: + console.log("Cross-tool protocol received ping message:", message); + (event.source as Window).postMessage(new MessagePong(), globalConfig.REMOTE_TOOL_URL); + break; + case MessageType.PONG: + console.log("Cross-tool protocol received unexpected pong message:", message); + break; + case MessageType.BUGREPORT: + console.log("Cross-tool protocol received bugreport message:", message); + await this.onMessageBugreportReceived(message as MessageBugReport); + break; + case MessageType.TIMESTAMP: + console.log("Cross-tool protocol received timestamp message:", message); + await this.onMessageTimestampReceived(message as MessageTimestamp); + break; + case MessageType.FILES: + console.log("Cross-tool protocol received unexpected files message", message); + break; + default: + console.log("Cross-tool protocol received unsupported message type:", message); + break; + } + } + + private async onMessageBugreportReceived(message: MessageBugReport) { + const timestamp = message.timestampNs !== undefined + ? new RealTimestamp(message.timestampNs) + : undefined; + this.onBugreportReceived(message.file, timestamp); + } + + private async onMessageTimestampReceived(message: MessageTimestamp) { + const timestamp = new RealTimestamp(message.timestampNs); + await this.onTimestampReceived(timestamp); + } +} + +export {CrossToolProtocol}; diff --git a/tools/winscope-ng/src/cross_tool/cross_tool_protocol_dependency_inversion.ts b/tools/winscope-ng/src/cross_tool/cross_tool_protocol_dependency_inversion.ts new file mode 100644 index 000000000..9573b94ac --- /dev/null +++ b/tools/winscope-ng/src/cross_tool/cross_tool_protocol_dependency_inversion.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 {RealTimestamp} from "common/trace/timestamp"; + +export type OnBugreportReceived = (bugreport: File, timestamp?: RealTimestamp) => Promise; +export type OnTimestampReceived = (timestamp: RealTimestamp) => Promise; + +export interface CrossToolProtocolDependencyInversion { + setOnBugreportReceived(callback: OnBugreportReceived): void; + setOnTimestampReceived(callback: OnTimestampReceived): void; + sendTimestamp(timestamp: RealTimestamp): void; +} diff --git a/tools/winscope-ng/src/cross_tool/cross_tool_protocol_stub.ts b/tools/winscope-ng/src/cross_tool/cross_tool_protocol_stub.ts new file mode 100644 index 000000000..a6c391643 --- /dev/null +++ b/tools/winscope-ng/src/cross_tool/cross_tool_protocol_stub.ts @@ -0,0 +1,39 @@ +/* + * 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 { + CrossToolProtocolDependencyInversion, + OnBugreportReceived, + OnTimestampReceived} from "cross_tool/cross_tool_protocol_dependency_inversion"; +import {RealTimestamp} from "common/trace/timestamp"; +import {FunctionUtils} from "common/utils/function_utils"; + +export class CrossToolProtocolStub implements CrossToolProtocolDependencyInversion { + onBugreportReceived: OnBugreportReceived = FunctionUtils.DO_NOTHING_ASYNC; + onTimestampReceived: OnTimestampReceived = FunctionUtils.DO_NOTHING_ASYNC; + + setOnBugreportReceived(callback: OnBugreportReceived) { + this.onBugreportReceived = callback; + } + + setOnTimestampReceived(callback: OnTimestampReceived) { + this.onTimestampReceived = callback; + } + + sendTimestamp(timestamp: RealTimestamp) { + // do nothing + } +} diff --git a/tools/winscope-ng/src/cross_tool/messages.ts b/tools/winscope-ng/src/cross_tool/messages.ts new file mode 100644 index 000000000..b02ca7c38 --- /dev/null +++ b/tools/winscope-ng/src/cross_tool/messages.ts @@ -0,0 +1,65 @@ +/* + * 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. + */ + +export enum MessageType { + UNKNOWN = 0, + PING, + PONG, + BUGREPORT, + TIMESTAMP, + FILES, +} + +export interface Message { + type: MessageType; +} + +export class MessagePing implements Message { + type = MessageType.PING; +} + +export class MessagePong implements Message { + type = MessageType.PONG; +} + +export class MessageBugReport implements Message { + type = MessageType.BUGREPORT; + + constructor( + public file: File, + public timestampNs?: bigint, + public issueId?: string + ) {} +} + +export class MessageTimestamp implements Message { + type = MessageType.TIMESTAMP; + + constructor( + public timestampNs: bigint, + public sections?: string[] + ) {} +} + +export class MessageFiles implements Message { + type = MessageType.FILES; + + constructor( + public files: File[], + public timestampNs?: bigint, + public issueId?: string + ) {} +} diff --git a/tools/winscope-ng/src/main.dev.ts b/tools/winscope-ng/src/main.dev.ts index 134f523ff..b42bfe79c 100644 --- a/tools/winscope-ng/src/main.dev.ts +++ b/tools/winscope-ng/src/main.dev.ts @@ -15,6 +15,12 @@ */ import {platformBrowserDynamic} from "@angular/platform-browser-dynamic"; import {AppModule} from "./app/app.module"; +import {globalConfig} from "common/utils/global_config"; + +globalConfig.set({ + MODE: "DEV", + REMOTE_TOOL_URL: "http://localhost:8081" +}); platformBrowserDynamic().bootstrapModule(AppModule) .catch(err => console.error(err)); diff --git a/tools/winscope-ng/src/test/e2e/cross_tool_protocol.spec.ts b/tools/winscope-ng/src/test/e2e/cross_tool_protocol.spec.ts new file mode 100644 index 000000000..ce4fb2359 --- /dev/null +++ b/tools/winscope-ng/src/test/e2e/cross_tool_protocol.spec.ts @@ -0,0 +1,160 @@ +/* + * 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, by, element, ElementFinder} from "protractor"; +import {E2eTestUtils} from "./utils"; + +describe("Cross-Tool Protocol", () => { + const WINSCOPE_URL = "http://localhost:8080"; + const REMOTE_TOOL_MOCK_URL = "http://localhost:8081"; + + const TIMESTAMP_IN_BUGREPORT_MESSAGE = "1670509911000000000"; + const TIMESTAMP_FROM_REMOTE_TOOL_TO_WINSCOPE = "1670509912000000000"; + const TIMESTAMP_FROM_WINSCOPE_TO_REMOTE_TOOL = "1670509913000000000"; + + beforeAll(async () => { + await browser.manage().timeouts().implicitlyWait(5000); + await checkServerIsUp("Remote tool mock", REMOTE_TOOL_MOCK_URL); + await checkServerIsUp("Winscope", WINSCOPE_URL); + }); + + beforeEach(async () => { + await browser.get(REMOTE_TOOL_MOCK_URL); + }); + + it("allows communication between remote tool and Winscope", async () => { + await openWinscopeTabFromRemoteTool(); + await waitWinscopeTabIsOpen(); + + await sendBugreportToWinscope(); + await checkWinscopeRenderedSurfaceFlingerView(); + await checkWinscopeRenderedAllViewTabs(); + await checkWinscopeAppliedTimestampInBugreportMessage(); + + await sendTimestampToWinscope(); + await checkWinscopeReceivedTimestamp(); + + await changeTimestampInWinscope(); + await checkRemoteToolReceivedTimestamp(); + }); + + const checkServerIsUp = async (name: string, url: string) => { + try { + await browser.get(url); + } catch (error) { + fail(`${name} server (${url}) looks down. Did you start it?`); + } + }; + + const openWinscopeTabFromRemoteTool = async () => { + await browser.switchTo().window(await getWindowHandleRemoteToolMock()); + const buttonElement = element(by.css(".button-open-winscope")); + await buttonElement.click(); + }; + + const sendBugreportToWinscope = async () => { + await browser.switchTo().window(await getWindowHandleRemoteToolMock()); + const inputFileElement = element(by.css(".button-upload-bugreport")); + await inputFileElement.sendKeys(E2eTestUtils.getFixturePath("bugreports/bugreport_stripped.zip")); + }; + + const waitWinscopeTabIsOpen = async () => { + await browser.wait(async () => { + const handles = await browser.getAllWindowHandles(); + return handles.length >= 2; + }, + 20000, + "The Winscope tab did not open"); + }; + + const checkWinscopeRenderedSurfaceFlingerView = async () => { + await browser.switchTo().window(await getWindowHandleWinscope()); + const viewerPresent = await element(by.css("viewer-surface-flinger")).isPresent(); + expect(viewerPresent).toBeTruthy(); + }; + + const checkWinscopeRenderedAllViewTabs = async () => { + const linkElements = await element.all(by.css(".tabs-navigation-bar a")); + + const actualLinks = await Promise.all( + (linkElements as ElementFinder[]).map(async linkElement => await linkElement.getText()) + ); + + const expectedLinks = [ + "Input Method Clients", + "Input Method Manager Service", + "Input Method Service", + "ProtoLog", + "Surface Flinger", + "Transactions", + "Window Manager", + ]; + + expect(actualLinks.sort()).toEqual(expectedLinks.sort()); + }; + + const checkWinscopeAppliedTimestampInBugreportMessage = async () => { + await browser.switchTo().window(await getWindowHandleWinscope()); + const inputElement = element(by.css("input[name=\"nsTimeInput\"]")); + const valueWithNsSuffix = await inputElement.getAttribute("value"); + expect(valueWithNsSuffix).toEqual(TIMESTAMP_IN_BUGREPORT_MESSAGE + " ns"); + }; + + const sendTimestampToWinscope = async () => { + await browser.switchTo().window(await getWindowHandleRemoteToolMock()); + const inputElement = element(by.css(".input-timestamp")); + await inputElement.sendKeys(TIMESTAMP_FROM_REMOTE_TOOL_TO_WINSCOPE); + const buttonElement = element(by.css(".button-send-timestamp")); + await buttonElement.click(); + }; + + const checkWinscopeReceivedTimestamp = async () => { + await browser.switchTo().window(await getWindowHandleWinscope()); + const inputElement = element(by.css("input[name=\"nsTimeInput\"]")); + const valueWithNsSuffix = await inputElement.getAttribute("value"); + expect(valueWithNsSuffix).toEqual(TIMESTAMP_FROM_REMOTE_TOOL_TO_WINSCOPE + " ns"); + }; + + const changeTimestampInWinscope = async () => { + await browser.switchTo().window(await getWindowHandleWinscope()); + const inputElement = element(by.css("input[name=\"nsTimeInput\"]")); + const inputStringStep1 = TIMESTAMP_FROM_WINSCOPE_TO_REMOTE_TOOL.slice(0, -1); + const inputStringStep2 = TIMESTAMP_FROM_WINSCOPE_TO_REMOTE_TOOL.slice(-1) + "\r\n"; + const script = + `document.querySelector("input[name=\\"nsTimeInput\\"]").value = "${inputStringStep1}"`; + await browser.executeScript(script); + await inputElement.sendKeys(inputStringStep2); + }; + + const checkRemoteToolReceivedTimestamp = async () => { + await browser.switchTo().window(await getWindowHandleRemoteToolMock()); + const paragraphElement = element(by.css(".paragraph-received-timestamp")); + const value = await paragraphElement.getText(); + expect(value).toEqual(TIMESTAMP_FROM_WINSCOPE_TO_REMOTE_TOOL); + }; + + const getWindowHandleRemoteToolMock = async (): Promise => { + const handles = await browser.getAllWindowHandles(); + expect(handles.length).toBeGreaterThan(0); + return handles[0]; + }; + + const getWindowHandleWinscope = async (): Promise => { + const handles = await browser.getAllWindowHandles(); + expect(handles.length).toEqual(2); + return handles[1]; + }; +}); diff --git a/tools/winscope-ng/src/test/remote_tool_mock/app.component.ts b/tools/winscope-ng/src/test/remote_tool_mock/app.component.ts new file mode 100644 index 000000000..dc3ac7e5c --- /dev/null +++ b/tools/winscope-ng/src/test/remote_tool_mock/app.component.ts @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ChangeDetectorRef, Component, Inject} from "@angular/core"; +import { + Message, + MessageBugReport, + MessagePing, + MessageTimestamp, + MessageType} from "cross_tool/messages"; +import {FunctionUtils} from "common/utils/function_utils"; + +@Component({ + selector: "app-root", + template: ` + Remote Tool Mock (simulates cross-tool protocol) + +
+

Open Winscope tab

+ + +
+

+ Send bugreport +

+ + +
+

+ Send timestamp [ns] +

+ + + +
+

+ Received timestamp: +

+

+

+ ` +}) +export class AppComponent { + static readonly TARGET = "http://localhost:8080"; + static readonly TIMESTAMP_IN_BUGREPORT_MESSAGE = 1670509911000000000n; + + private winscope: WindowProxy|null = null; + private isWinscopeUp = false; + private onMessagePongReceived = FunctionUtils.DO_NOTHING; + + constructor(@Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef) { + window.addEventListener("message", (event) => { + this.onMessageReceived(event); + }); + } + + public async onButtonOpenWinscopeClick() { + this.openWinscope(); + await this.waitWinscopeUp(); + } + + public async onUploadBugreport(event: Event) { + const [file, buffer] = await this.readInputFile(event); + this.sendBugreport(file, buffer); + } + + public onButtonSendTimestampClick() { + const inputTimestampElement = + document.querySelector(".input-timestamp")! as HTMLInputElement; + this.sendTimestamp(BigInt(inputTimestampElement.value)); + } + + private openWinscope() { + this.printStatus("OPENING WINSCOPE"); + + this.winscope = window.open(AppComponent.TARGET); + if (!this.winscope) { + throw new Error("Failed to open winscope"); + } + + this.printStatus("OPENED WINSCOPE"); + } + + private async waitWinscopeUp() { + this.printStatus("WAITING WINSCOPE UP"); + + const promise = new Promise((resolve) => { + this.onMessagePongReceived = () => { + this.isWinscopeUp = true; + resolve(); + }; + }); + + setTimeout(async () => { + while (!this.isWinscopeUp) { + this.winscope!.postMessage( + new MessagePing(), + AppComponent.TARGET + ); + await this.sleep(10); + } + }, 0); + + await promise; + + this.printStatus("DONE WAITING (WINSCOPE IS UP)"); + } + + private sendBugreport(file: File, buffer: ArrayBuffer) { + this.printStatus("SENDING BUGREPORT"); + + this.winscope!.postMessage( + new MessageBugReport(file, AppComponent.TIMESTAMP_IN_BUGREPORT_MESSAGE), + AppComponent.TARGET + ); + + this.printStatus("SENT BUGREPORT"); + } + + private sendTimestamp(value: bigint) { + this.printStatus("SENDING TIMESTAMP"); + + this.winscope!.postMessage( + new MessageTimestamp(value), + AppComponent.TARGET + ); + + this.printStatus("SENT TIMESTAMP"); + } + + private onMessageReceived(event: MessageEvent) { + const message = event.data as Message; + if (!message.type) { + console.log("Cross-tool protocol received unrecognized message:", message); + return; + } + + switch (message.type) { + case MessageType.PING: + console.log("Cross-tool protocol received unexpected ping message:", message); + break; + case MessageType.PONG: + this.onMessagePongReceived(); + break; + case MessageType.BUGREPORT: + console.log("Cross-tool protocol received unexpected bugreport message:", message); + break; + case MessageType.TIMESTAMP: + console.log("Cross-tool protocol received timestamp message:", message); + this.onMessageTimestampReceived(message as MessageTimestamp); + break; + case MessageType.FILES: + console.log("Cross-tool protocol received unexpected files message:", message); + break; + default: + console.log("Cross-tool protocol received unrecognized message:", message); + break; + } + } + + private onMessageTimestampReceived(message: MessageTimestamp) { + const paragraph = + document.querySelector(".paragraph-received-timestamp") as HTMLParagraphElement; + paragraph.textContent = message.timestampNs.toString(); + this.changeDetectorRef.detectChanges(); + } + + private printStatus(status: string) { + console.log("STATUS: " + status); + } + + private sleep(ms: number): Promise { + return new Promise( resolve => setTimeout(resolve, ms) ); + } + + private async readInputFile(event: Event): Promise<[File, ArrayBuffer]> { + const files: FileList|null = (event?.target as HTMLInputElement)?.files; + + if (!files || !files[0]) { + throw new Error("Failed to read input files"); + } + + return [files[0], await files[0].arrayBuffer()]; + } +} diff --git a/tools/winscope-ng/src/test/remote_tool_mock/app.module.ts b/tools/winscope-ng/src/test/remote_tool_mock/app.module.ts new file mode 100644 index 000000000..8b52449c2 --- /dev/null +++ b/tools/winscope-ng/src/test/remote_tool_mock/app.module.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 {CommonModule} from "@angular/common"; +import {NgModule} from "@angular/core"; +import {BrowserModule} from "@angular/platform-browser"; +import {AppComponent} from "./app.component"; + +@NgModule({ + declarations: [ + AppComponent, + ], + imports: [ + BrowserModule, + CommonModule, + ], + bootstrap: [AppComponent] +}) +class AppModule { +} + +export {AppModule}; diff --git a/tools/winscope-ng/src/test/remote_tool_mock/index.html b/tools/winscope-ng/src/test/remote_tool_mock/index.html new file mode 100644 index 000000000..4bc3f4b5c --- /dev/null +++ b/tools/winscope-ng/src/test/remote_tool_mock/index.html @@ -0,0 +1,25 @@ + + + + + + ABT Mock + + + + + diff --git a/tools/winscope-ng/src/test/remote_tool_mock/main.ts b/tools/winscope-ng/src/test/remote_tool_mock/main.ts new file mode 100644 index 000000000..da5885dd8 --- /dev/null +++ b/tools/winscope-ng/src/test/remote_tool_mock/main.ts @@ -0,0 +1,20 @@ +/* + * 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 {platformBrowserDynamic} from "@angular/platform-browser-dynamic"; +import {AppModule} from "./app.module"; + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/tools/winscope-ng/src/test/remote_tool_mock/polyfills.ts b/tools/winscope-ng/src/test/remote_tool_mock/polyfills.ts new file mode 100644 index 000000000..ef841f852 --- /dev/null +++ b/tools/winscope-ng/src/test/remote_tool_mock/polyfills.ts @@ -0,0 +1,53 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes recent versions of Safari, Chrome (including + * Opera), Edge on the desktop, and iOS and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import "zone.js"; // Included with Angular CLI. + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/tools/winscope-ng/src/test/remote_tool_mock/webpack.config.js b/tools/winscope-ng/src/test/remote_tool_mock/webpack.config.js new file mode 100644 index 000000000..59e8a4364 --- /dev/null +++ b/tools/winscope-ng/src/test/remote_tool_mock/webpack.config.js @@ -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. + */ + +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin'); + +module.exports = { + resolve: { + extensions: [".ts", ".js", ".css"], + modules: [ + __dirname + "/../../../node_modules", + __dirname + "/../../../src", + __dirname + ] + }, + + module: { + rules:[ + { + test: /\.ts$/, + use: ["ts-loader", "angular2-template-loader"] + }, + { + test: /\.html$/, + use: ["html-loader"] + }, + { + test: /\.css$/, + use: ["style-loader", "css-loader"] + }, + { + test: /\.s[ac]ss$/i, + use: ["style-loader", "css-loader", "sass-loader"] + } + ] + }, + + mode: "development", + + entry: { + polyfills: __dirname + "/polyfills.ts", + app: __dirname + "/main.ts" + }, + + output: { + path: __dirname + "/../../../dist/remote_tool_mock", + publicPath: "/", + filename: "js/[name].[hash].js", + chunkFilename: "js/[name].[id].[hash].chunk.js", + }, + + devtool: "source-map", + + plugins: [ + new HtmlWebpackPlugin({ + template: __dirname + "/index.html", + inject: "body", + inlineSource: ".(css|js)$", + }), + new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin), + ] +}; diff --git a/tools/winscope-ng/webpack.config.common.js b/tools/winscope-ng/webpack.config.common.js index a68b15ce3..1bfeca7f0 100644 --- a/tools/winscope-ng/webpack.config.common.js +++ b/tools/winscope-ng/webpack.config.common.js @@ -14,7 +14,6 @@ * limitations under the License. */ const path = require("path"); -const HtmlWebpackPlugin = require("html-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin"); module.exports = { @@ -63,14 +62,6 @@ module.exports = { ] }, - plugins: [ - new HtmlWebpackPlugin({ - template: "src/index.html", - inject: "body", - inlineSource: ".(css|js)$", - }) - ], - optimization: { minimizer: [ new TerserPlugin({ diff --git a/tools/winscope-ng/webpack.config.dev.js b/tools/winscope-ng/webpack.config.dev.js index 31a835454..a7c004a84 100644 --- a/tools/winscope-ng/webpack.config.dev.js +++ b/tools/winscope-ng/webpack.config.dev.js @@ -15,6 +15,7 @@ */ const {merge} = require("webpack-merge"); const configCommon = require("./webpack.config.common"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); const configDev = { mode: "development", @@ -27,6 +28,13 @@ const configDev = { app: "./src/main.dev.ts" }, devtool: "source-map", + plugins: [ + new HtmlWebpackPlugin({ + template: "src/index.html", + inject: "body", + inlineSource: ".(css|js)$", + }) + ] }; module.exports = merge(configCommon, configDev); diff --git a/tools/winscope-ng/webpack.config.prod.js b/tools/winscope-ng/webpack.config.prod.js index 2328fd0a8..755387934 100644 --- a/tools/winscope-ng/webpack.config.prod.js +++ b/tools/winscope-ng/webpack.config.prod.js @@ -60,6 +60,11 @@ const configProd = { }, }, plugins: [ + new HtmlWebpackPlugin({ + template: "src/index.html", + inject: "body", + inlineSource: ".(css|js)$", + }), new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin), ] };