Merge changes Id61114b8,I3451e8a8,I01a258ba

* changes:
  Add AbtChromeExtensionProtocol
  Add GlobalConfig to customize Winscope behavior for testing
  Cross-tool protocol
This commit is contained in:
Kean Mariotti
2022-12-16 10:19:56 +00:00
committed by Android (Google) Code Review
34 changed files with 1624 additions and 134 deletions

View File

@@ -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": {

View File

@@ -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",

View File

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

View File

@@ -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<void>;
export interface AbtChromeExtensionProtocolDependencyInversion {
setOnBugAttachmentsReceived(callback: OnBugAttachmentsReceived): void;
run(): void;
}

View File

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

View File

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

View File

@@ -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";
<collect-traces
class="collect-traces-card homepage-card"
[traceData]="traceData"
(traceDataLoaded)="onTraceDataLoaded()"
(traceDataLoaded)="mediator.onWinscopeTraceDataLoaded()"
[store]="store"
></collect-traces>
<upload-traces
class="upload-traces-card homepage-card"
[traceData]="traceData"
(traceDataLoaded)="onTraceDataLoaded()"
(traceDataLoaded)="mediator.onWinscopeTraceDataLoaded()"
></upload-traces>
</div>
</div>
@@ -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);

View File

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

View File

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

View File

@@ -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<TimelineComponent>;
@@ -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;
});

View File

@@ -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() {

View File

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

View File

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

View File

@@ -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 () => {

View File

@@ -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<void>(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<TraceType>(traceTypes), storage);
this.viewers = new ViewerFactory().createViewers(new Set<TraceType>(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<void>) {
if (this.isChangingCurrentTimestamp) {
return;
}
this.isChangingCurrentTimestamp = true;
try {
await op();
} finally {
this.isChangingCurrentTimestamp = false;
}
}
}
export { Mediator };

View File

@@ -43,7 +43,7 @@ describe("TimelineData", () => {
beforeEach(() => {
timelineData = new TimelineData();
timelineData.setOnCurrentTimestampChangedCallback(timestamp => {
timelineData.setOnCurrentTimestampChanged(timestamp => {
timestampChangedObserver.onCurrentTimestampChanged(timestamp);
});
});

View File

@@ -56,7 +56,7 @@ export class TimelineData {
this.onCurrentTimestampChanged(this.getCurrentTimestamp());
}
setOnCurrentTimestampChangedCallback(callback: TimestampCallbackType) {
setOnCurrentTimestampChanged(callback: TimestampCallbackType) {
this.onCurrentTimestampChanged = callback;
}

View File

@@ -20,4 +20,8 @@ export class FunctionUtils {
static readonly DO_NOTHING = () => {
// do nothing
};
static readonly DO_NOTHING_ASYNC = (): Promise<void> => {
return Promise.resolve();
};
}

View File

@@ -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();

View File

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

View File

@@ -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<void>;
export type OnTimestampReceived = (timestamp: RealTimestamp) => Promise<void>;
export interface CrossToolProtocolDependencyInversion {
setOnBugreportReceived(callback: OnBugreportReceived): void;
setOnTimestampReceived(callback: OnTimestampReceived): void;
sendTimestamp(timestamp: RealTimestamp): void;
}

View File

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

View File

@@ -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
) {}
}

View File

@@ -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));

View File

@@ -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<string> => {
const handles = await browser.getAllWindowHandles();
expect(handles.length).toBeGreaterThan(0);
return handles[0];
};
const getWindowHandleWinscope = async (): Promise<string> => {
const handles = await browser.getAllWindowHandles();
expect(handles.length).toEqual(2);
return handles[1];
};
});

View File

@@ -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: `
<span class="app-title">Remote Tool Mock (simulates cross-tool protocol)</span>
<hr>
<p>Open Winscope tab</p>
<input class="button-open-winscope" type="button" value="Open"
(click)="onButtonOpenWinscopeClick()">
<hr>
<p>
Send bugreport
</p>
<input class="button-upload-bugreport" type="file" value=""
(change)="onUploadBugreport($event)">
<hr>
<p>
Send timestamp [ns]
</p>
<input class="input-timestamp" type="number" id="name" name="name">
<input class="button-send-timestamp" type="button" value="Send"
(click)="onButtonSendTimestampClick()">
<hr>
<p>
Received timestamp:
</p>
<p class="paragraph-received-timestamp">
</p>
`
})
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<void>((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<void> {
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()];
}
}

View File

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

View File

@@ -0,0 +1,25 @@
<!--
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.
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ABT Mock</title>
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@@ -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));

View File

@@ -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
*/

View File

@@ -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),
]
};

View File

@@ -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({

View File

@@ -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);

View File

@@ -60,6 +60,11 @@ const configProd = {
},
},
plugins: [
new HtmlWebpackPlugin({
template: "src/index.html",
inject: "body",
inlineSource: ".(css|js)$",
}),
new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin),
]
};