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 {assertDefined, assertUnreachable} from 'common/assert_utils';
18import {FunctionUtils} from 'common/function_utils';
19import {Timestamp} from 'common/time';
20import {RemoteToolTimestampConverter} from 'common/timestamp_converter';
21import {
22  RemoteToolFilesReceived,
23  RemoteToolTimestampReceived,
24  WinscopeEvent,
25  WinscopeEventType,
26} from 'messaging/winscope_event';
27import {
28  EmitEvent,
29  WinscopeEventEmitter,
30} from 'messaging/winscope_event_emitter';
31import {WinscopeEventListener} from 'messaging/winscope_event_listener';
32import {
33  Message,
34  MessageBugReport,
35  MessageFiles,
36  MessagePong,
37  MessageTimestamp,
38  MessageType,
39  TimestampType,
40} from './messages';
41import {OriginAllowList} from './origin_allow_list';
42
43class RemoteTool {
44  timestampType?: TimestampType;
45
46  constructor(readonly window: Window, readonly origin: string) {}
47}
48
49export class CrossToolProtocol
50  implements WinscopeEventEmitter, WinscopeEventListener
51{
52  private remoteTool?: RemoteTool;
53  private emitEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
54  private timestampConverter: RemoteToolTimestampConverter;
55  private allowTimestampSync = true;
56
57  constructor(timestampConverter: RemoteToolTimestampConverter) {
58    this.timestampConverter = timestampConverter;
59
60    window.addEventListener('message', async (event) => {
61      await this.onMessageReceived(event);
62    });
63  }
64
65  setEmitEvent(callback: EmitEvent) {
66    this.emitEvent = callback;
67  }
68
69  async onWinscopeEvent(event: WinscopeEvent) {
70    await event.visit(
71      WinscopeEventType.TRACE_POSITION_UPDATE,
72      async (event) => {
73        if (
74          !this.remoteTool ||
75          !this.remoteTool.timestampType ||
76          !this.allowTimestampSync
77        ) {
78          return;
79        }
80
81        const timestampNs = this.getTimestampNsForRemoteTool(
82          event.position.timestamp,
83        );
84        if (timestampNs === undefined) {
85          return;
86        }
87
88        const message = new MessageTimestamp(
89          timestampNs,
90          this.remoteTool.timestampType,
91        );
92        this.remoteTool.window.postMessage(message, this.remoteTool.origin);
93        console.log('Cross-tool protocol sent timestamp message:', message);
94      },
95    );
96  }
97
98  isConnected() {
99    return this.remoteTool !== undefined;
100  }
101
102  setAllowTimestampSync(value: boolean) {
103    this.allowTimestampSync = value;
104  }
105
106  getAllowTimestampSync() {
107    return this.allowTimestampSync;
108  }
109
110  private async onMessageReceived(event: MessageEvent) {
111    if (!OriginAllowList.isAllowed(event.origin)) {
112      return;
113    }
114
115    const message = event.data as Message;
116    if (message.type === undefined) {
117      return;
118    }
119
120    if (!this.remoteTool) {
121      this.remoteTool = new RemoteTool(event.source as Window, event.origin);
122    }
123
124    switch (message.type) {
125      case MessageType.PING:
126        console.log('Cross-tool protocol received ping message:', message);
127        (event.source as Window).postMessage(new MessagePong(), event.origin);
128        break;
129      case MessageType.PONG:
130        console.log(
131          'Cross-tool protocol received unexpected pong message:',
132          message,
133        );
134        break;
135      case MessageType.BUGREPORT:
136        console.log('Cross-tool protocol received bugreport message:', message);
137        await this.onMessageBugreportReceived(message as MessageBugReport);
138        console.log(
139          'Cross-tool protocol processed bugreport message:',
140          message,
141        );
142        break;
143      case MessageType.TIMESTAMP:
144        console.log('Cross-tool protocol received timestamp message:', message);
145        await this.onMessageTimestampReceived(message as MessageTimestamp);
146        console.log(
147          'Cross-tool protocol processed timestamp message:',
148          message,
149        );
150        break;
151      case MessageType.FILES:
152        console.log('Cross-tool protocol received files message:', message);
153        await this.onMessageFilesReceived(message as MessageFiles);
154        console.log('Cross-tool protocol processed files message:', message);
155        console.log(
156          'Cross-tool protocol received unexpected files message',
157          message,
158        );
159        break;
160      default:
161        console.log(
162          'Cross-tool protocol received unsupported message type:',
163          message,
164        );
165        break;
166    }
167  }
168
169  private async onMessageBugreportReceived(message: MessageBugReport) {
170    this.setRemoteToolTimestampTypeIfNeeded(message.timestampType);
171    const deferredTimestamp = this.makeDeferredTimestampForWinscope(
172      message.timestampNs,
173    );
174    await this.emitEvent(
175      new RemoteToolFilesReceived([message.file], deferredTimestamp),
176    );
177  }
178
179  private async onMessageFilesReceived(message: MessageFiles) {
180    this.setRemoteToolTimestampTypeIfNeeded(message.timestampType);
181    const deferredTimestamp = this.makeDeferredTimestampForWinscope(
182      message.timestampNs,
183    );
184    await this.emitEvent(
185      new RemoteToolFilesReceived(message.files, deferredTimestamp),
186    );
187  }
188
189  private async onMessageTimestampReceived(message: MessageTimestamp) {
190    if (!this.allowTimestampSync) {
191      return;
192    }
193    this.setRemoteToolTimestampTypeIfNeeded(message.timestampType);
194    const deferredTimestamp = this.makeDeferredTimestampForWinscope(
195      message.timestampNs,
196    );
197    await this.emitEvent(
198      new RemoteToolTimestampReceived(assertDefined(deferredTimestamp)),
199    );
200  }
201
202  private setRemoteToolTimestampTypeIfNeeded(type: TimestampType | undefined) {
203    const remoteTool = assertDefined(this.remoteTool);
204
205    if (remoteTool.timestampType !== undefined) {
206      return;
207    }
208
209    // Default to CLOCK_REALTIME for backward compatibility.
210    // The initial protocol's version didn't provide an explicit timestamp type
211    // and all timestamps were supposed to be CLOCK_REALTIME.
212    remoteTool.timestampType = type ?? TimestampType.CLOCK_REALTIME;
213  }
214
215  private getTimestampNsForRemoteTool(
216    timestamp: Timestamp,
217  ): bigint | undefined {
218    const timestampType = this.remoteTool?.timestampType;
219    switch (timestampType) {
220      case undefined:
221        return undefined;
222      case TimestampType.UNKNOWN:
223        return undefined;
224      case TimestampType.CLOCK_BOOTTIME:
225        return this.timestampConverter.tryGetBootTimeNs(timestamp);
226      case TimestampType.CLOCK_REALTIME:
227        return this.timestampConverter.tryGetRealTimeNs(timestamp);
228      default:
229        assertUnreachable(timestampType);
230    }
231  }
232
233  // Make a deferred timestamp: a lambda meant to be executed at a later point to create a
234  // timestamp. The lambda is needed to defer timestamp creation to the point where traces
235  // are loaded into TracePipeline and TimestampConverter is properly initialized and ready
236  // to instantiate timestamps.
237  private makeDeferredTimestampForWinscope(
238    timestampNs: bigint | undefined,
239  ): (() => Timestamp | undefined) | undefined {
240    const timestampType = assertDefined(this.remoteTool?.timestampType);
241
242    if (timestampNs === undefined || timestampType === undefined) {
243      return undefined;
244    }
245
246    switch (timestampType) {
247      case TimestampType.UNKNOWN:
248        return undefined;
249      case TimestampType.CLOCK_BOOTTIME:
250        return () => {
251          try {
252            return this.timestampConverter.makeTimestampFromBootTimeNs(
253              timestampNs,
254            );
255          } catch (error) {
256            return undefined;
257          }
258        };
259      case TimestampType.CLOCK_REALTIME:
260        return () => {
261          try {
262            return this.timestampConverter.makeTimestampFromRealNs(timestampNs);
263          } catch (error) {
264            return undefined;
265          }
266        };
267      default:
268        assertUnreachable(timestampType);
269    }
270  }
271}
272