1/*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {ChangeDetectorRef, Component, Inject} from '@angular/core';
18import {assertDefined, assertUnreachable} from 'common/assert_utils';
19import {FunctionUtils} from 'common/function_utils';
20import {TimeUtils} from 'common/time_utils';
21import {
22  Message,
23  MessageBugReport,
24  MessageFiles,
25  MessagePing,
26  MessageTimestamp,
27  MessageType,
28  TimestampType,
29} from 'cross_tool/messages';
30
31@Component({
32  selector: 'app-root',
33  template: `
34    <span class="app-title">Remote Tool Mock (simulates cross-tool protocol)</span>
35
36    <hr/>
37    <p>Open Winscope tab</p>
38    <input
39        class="button-open-winscope"
40        type="button"
41        value="Open"
42        (click)="onButtonOpenWinscopeClick()"/>
43
44    <hr/>
45    <p>Send bugreport</p>
46    <input
47        class="button-send-bugreport"
48        type="file"
49        value=""
50        (change)="onButtonSendBugreportClick($event)"/>
51
52    <hr/>
53    <p>Send file</p>
54    <input
55        class="button-send-files"
56        type="file"
57        value=""
58        (change)="onButtonSendFilesClick($event)"/>
59
60    <hr/>
61    <p>Send timestamp [ns]</p>
62    <input class="input-timestamp" type="number" id="name" name="name"/>
63    <input
64        class="button-send-realtime-timestamp"
65        type="button"
66        value="Send"
67        (click)="onButtonSendRealtimeTimestampClick()"/>
68    <input
69        class="button-send-boottime-timestamp"
70        type="button"
71        value="Send"
72        (click)="onButtonSendBoottimeTimestampClick()"/>
73    <hr/>
74    <p>Received realtime timestamp:</p>
75    <p class="paragraph-received-realtime-timestamp"></p>
76    <p>Received boottime timestamp:</p>
77    <p class="paragraph-received-boottime-timestamp"></p>
78  `,
79})
80export class AppComponent {
81  static readonly TARGET = 'http://localhost:8080';
82  static readonly TIMESTAMP_IN_BUGREPORT_MESSAGE = 1670509911000000000n;
83  static readonly TIMESTAMP_IN_FILES_MESSAGE = 15725894416n;
84
85  private winscope: Window | null = null;
86  private isWinscopeUp = false;
87  private onMessagePongReceived = FunctionUtils.DO_NOTHING;
88
89  constructor(
90    @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef,
91  ) {
92    window.addEventListener('message', (event) => {
93      this.onMessageReceived(event);
94    });
95  }
96
97  async onButtonOpenWinscopeClick() {
98    this.openWinscope();
99    await this.waitWinscopeUp();
100  }
101
102  async onButtonSendBugreportClick(event: Event) {
103    const file = await this.readInputFile(event);
104    this.sendBugreport(file);
105  }
106
107  async onButtonSendFilesClick(event: Event) {
108    const file = await this.readInputFile(event);
109    this.sendFiles([file]);
110  }
111
112  onButtonSendRealtimeTimestampClick() {
113    const inputTimestampElement = assertDefined(
114      document.querySelector('.input-timestamp'),
115    ) as HTMLInputElement;
116    this.sendTimestamp(
117      BigInt(inputTimestampElement.value),
118      TimestampType.CLOCK_REALTIME,
119    );
120  }
121
122  onButtonSendBoottimeTimestampClick() {
123    const inputTimestampElement = assertDefined(
124      document.querySelector('.input-timestamp'),
125    ) as HTMLInputElement;
126    this.sendTimestamp(
127      BigInt(inputTimestampElement.value),
128      TimestampType.CLOCK_BOOTTIME,
129    );
130  }
131
132  private openWinscope() {
133    this.printStatus('OPENING WINSCOPE');
134
135    this.winscope = window.open(AppComponent.TARGET);
136    if (!this.winscope) {
137      throw new Error('Failed to open winscope');
138    }
139
140    this.printStatus('OPENED WINSCOPE');
141  }
142
143  private async waitWinscopeUp() {
144    this.printStatus('WAITING WINSCOPE UP');
145
146    const promise = new Promise<void>((resolve) => {
147      this.onMessagePongReceived = () => {
148        this.isWinscopeUp = true;
149        resolve();
150      };
151    });
152
153    setTimeout(async () => {
154      while (!this.isWinscopeUp) {
155        assertDefined(this.winscope).postMessage(
156          new MessagePing(),
157          AppComponent.TARGET,
158        );
159        await TimeUtils.sleepMs(10);
160      }
161    }, 0);
162
163    await promise;
164
165    this.printStatus('DONE WAITING (WINSCOPE IS UP)');
166  }
167
168  private sendBugreport(file: File) {
169    this.printStatus('SENDING BUGREPORT');
170
171    assertDefined(this.winscope).postMessage(
172      new MessageBugReport(file, AppComponent.TIMESTAMP_IN_BUGREPORT_MESSAGE),
173      AppComponent.TARGET,
174    );
175
176    this.printStatus('SENT BUGREPORT');
177  }
178
179  private sendFiles(files: File[]) {
180    this.printStatus('SENDING FILES');
181
182    assertDefined(this.winscope).postMessage(
183      new MessageFiles(
184        files,
185        AppComponent.TIMESTAMP_IN_FILES_MESSAGE,
186        TimestampType.CLOCK_BOOTTIME,
187      ),
188      AppComponent.TARGET,
189    );
190
191    this.printStatus('SENT FILES');
192  }
193
194  private sendTimestamp(value: bigint, type: TimestampType) {
195    this.printStatus('SENDING TIMESTAMP');
196
197    assertDefined(this.winscope).postMessage(
198      new MessageTimestamp(value, type),
199      AppComponent.TARGET,
200    );
201
202    this.printStatus('SENT TIMESTAMP');
203  }
204
205  private onMessageReceived(event: MessageEvent) {
206    const message = event.data as Message;
207    if (!message.type) {
208      console.log(
209        'Cross-tool protocol received unrecognized message:',
210        message,
211      );
212      return;
213    }
214
215    switch (message.type) {
216      case MessageType.PING:
217        console.log(
218          'Cross-tool protocol received unexpected ping message:',
219          message,
220        );
221        break;
222      case MessageType.PONG:
223        this.onMessagePongReceived();
224        break;
225      case MessageType.BUGREPORT:
226        console.log(
227          'Cross-tool protocol received unexpected bugreport message:',
228          message,
229        );
230        break;
231      case MessageType.TIMESTAMP:
232        console.log('Cross-tool protocol received timestamp message:', message);
233        this.onMessageTimestampReceived(message as MessageTimestamp);
234        break;
235      case MessageType.FILES:
236        console.log(
237          'Cross-tool protocol received unexpected files message:',
238          message,
239        );
240        break;
241      default:
242        console.log(
243          'Cross-tool protocol received unrecognized message:',
244          message,
245        );
246        break;
247    }
248  }
249
250  private onMessageTimestampReceived(message: MessageTimestamp) {
251    let paragraph: HTMLParagraphElement | undefined;
252
253    const timestampType = assertDefined(message.timestampType);
254    switch (timestampType) {
255      case TimestampType.UNKNOWN:
256        throw Error("Winscope shouldn't send timestamps with UNKNOWN type");
257      case TimestampType.CLOCK_BOOTTIME: {
258        paragraph = document.querySelector(
259          '.paragraph-received-boottime-timestamp',
260        ) as HTMLParagraphElement;
261        break;
262      }
263      case TimestampType.CLOCK_REALTIME: {
264        paragraph = document.querySelector(
265          '.paragraph-received-realtime-timestamp',
266        ) as HTMLParagraphElement;
267        break;
268      }
269      default:
270        assertUnreachable(timestampType);
271    }
272
273    paragraph.textContent = message.timestampNs.toString();
274    this.changeDetectorRef.detectChanges();
275  }
276
277  private printStatus(status: string) {
278    console.log('STATUS: ' + status);
279  }
280
281  private async readInputFile(event: Event): Promise<File> {
282    const files: FileList | null = (event?.target as HTMLInputElement)?.files;
283
284    if (!files || !files[0]) {
285      throw new Error('Failed to read input files');
286    }
287
288    return files[0];
289  }
290}
291