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 {Timestamp} from 'common/time';
18import {TimeUtils} from 'common/time_utils';
19import {CrossToolProtocol} from 'cross_tool/cross_tool_protocol';
20import {Analytics} from 'logging/analytics';
21import {ProgressListener} from 'messaging/progress_listener';
22import {UserNotificationsListener} from 'messaging/user_notifications_listener';
23import {UserWarning} from 'messaging/user_warning';
24import {
25  ActiveTraceChanged,
26  ExpandedTimelineToggled,
27  TracePositionUpdate,
28  ViewersLoaded,
29  ViewersUnloaded,
30  WinscopeEvent,
31  WinscopeEventType,
32} from 'messaging/winscope_event';
33import {WinscopeEventEmitter} from 'messaging/winscope_event_emitter';
34import {WinscopeEventListener} from 'messaging/winscope_event_listener';
35import {TraceEntry} from 'trace/trace';
36import {TracePosition} from 'trace/trace_position';
37import {View, Viewer, ViewType} from 'viewers/viewer';
38import {ViewerFactory} from 'viewers/viewer_factory';
39import {FilesSource} from './files_source';
40import {TimelineData} from './timeline_data';
41import {TracePipeline} from './trace_pipeline';
42
43export class Mediator {
44  private abtChromeExtensionProtocol: WinscopeEventEmitter &
45    WinscopeEventListener;
46  private crossToolProtocol: CrossToolProtocol;
47  private uploadTracesComponent?: ProgressListener;
48  private collectTracesComponent?: ProgressListener & WinscopeEventListener;
49  private traceViewComponent?: WinscopeEventEmitter & WinscopeEventListener;
50  private timelineComponent?: WinscopeEventEmitter & WinscopeEventListener;
51  private appComponent: WinscopeEventListener;
52  private userNotificationsListener: UserNotificationsListener;
53  private storage: Storage;
54
55  private tracePipeline: TracePipeline;
56  private timelineData: TimelineData;
57  private viewers: Viewer[] = [];
58  private focusedTabView: undefined | View;
59  private areViewersLoaded = false;
60  private lastRemoteToolDeferredTimestampReceived?: () => Timestamp | undefined;
61  private currentProgressListener?: ProgressListener;
62
63  constructor(
64    tracePipeline: TracePipeline,
65    timelineData: TimelineData,
66    abtChromeExtensionProtocol: WinscopeEventEmitter & WinscopeEventListener,
67    crossToolProtocol: CrossToolProtocol,
68    appComponent: WinscopeEventListener,
69    userNotificationsListener: UserNotificationsListener,
70    storage: Storage,
71  ) {
72    this.tracePipeline = tracePipeline;
73    this.timelineData = timelineData;
74    this.abtChromeExtensionProtocol = abtChromeExtensionProtocol;
75    this.crossToolProtocol = crossToolProtocol;
76    this.appComponent = appComponent;
77    this.userNotificationsListener = userNotificationsListener;
78    this.storage = storage;
79
80    this.crossToolProtocol.setEmitEvent(async (event) => {
81      await this.onWinscopeEvent(event);
82    });
83
84    this.abtChromeExtensionProtocol.setEmitEvent(async (event) => {
85      await this.onWinscopeEvent(event);
86    });
87  }
88
89  setUploadTracesComponent(component: ProgressListener | undefined) {
90    this.uploadTracesComponent = component;
91  }
92
93  setCollectTracesComponent(
94    component: (ProgressListener & WinscopeEventListener) | undefined,
95  ) {
96    this.collectTracesComponent = component;
97  }
98
99  setTraceViewComponent(
100    component: (WinscopeEventEmitter & WinscopeEventListener) | undefined,
101  ) {
102    this.traceViewComponent = component;
103    this.traceViewComponent?.setEmitEvent(async (event) => {
104      await this.onWinscopeEvent(event);
105    });
106  }
107
108  setTimelineComponent(
109    component: (WinscopeEventEmitter & WinscopeEventListener) | undefined,
110  ) {
111    this.timelineComponent = component;
112    this.timelineComponent?.setEmitEvent(async (event) => {
113      await this.onWinscopeEvent(event);
114    });
115  }
116
117  async onWinscopeEvent(event: WinscopeEvent) {
118    await event.visit(WinscopeEventType.APP_INITIALIZED, async (event) => {
119      await this.abtChromeExtensionProtocol.onWinscopeEvent(event);
120    });
121
122    await event.visit(WinscopeEventType.APP_FILES_UPLOADED, async (event) => {
123      this.currentProgressListener = this.uploadTracesComponent;
124      await this.loadFiles(event.files, FilesSource.UPLOADED);
125    });
126
127    await event.visit(WinscopeEventType.APP_FILES_COLLECTED, async (event) => {
128      this.currentProgressListener = this.collectTracesComponent;
129      await this.loadFiles(event.files, FilesSource.COLLECTED);
130      await this.loadViewers();
131    });
132
133    await event.visit(WinscopeEventType.APP_RESET_REQUEST, async () => {
134      await this.resetAppToInitialState();
135    });
136
137    await event.visit(
138      WinscopeEventType.APP_REFRESH_DUMPS_REQUEST,
139      async (event) => {
140        await this.resetAppToInitialState();
141        await this.collectTracesComponent?.onWinscopeEvent(event);
142      },
143    );
144
145    await event.visit(WinscopeEventType.APP_TRACE_VIEW_REQUEST, async () => {
146      await this.loadViewers();
147    });
148
149    await event.visit(
150      WinscopeEventType.REMOTE_TOOL_DOWNLOAD_START,
151      async () => {
152        Analytics.Tracing.logOpenFromABT();
153        await this.resetAppToInitialState();
154        this.currentProgressListener = this.uploadTracesComponent;
155        this.currentProgressListener?.onProgressUpdate(
156          'Downloading files...',
157          undefined,
158        );
159      },
160    );
161
162    await event.visit(
163      WinscopeEventType.REMOTE_TOOL_FILES_RECEIVED,
164      async (event) => {
165        await this.processRemoteFilesReceived(
166          event.files,
167          FilesSource.REMOTE_TOOL,
168        );
169        if (event.deferredTimestamp) {
170          await this.processRemoteToolDeferredTimestampReceived(
171            event.deferredTimestamp,
172          );
173        }
174      },
175    );
176
177    await event.visit(
178      WinscopeEventType.REMOTE_TOOL_TIMESTAMP_RECEIVED,
179      async (event) => {
180        await this.processRemoteToolDeferredTimestampReceived(
181          event.deferredTimestamp,
182        );
183      },
184    );
185
186    await event.visit(
187      WinscopeEventType.TABBED_VIEW_SWITCH_REQUEST,
188      async (event) => {
189        await this.traceViewComponent?.onWinscopeEvent(event);
190      },
191    );
192
193    await event.visit(WinscopeEventType.TABBED_VIEW_SWITCHED, async (event) => {
194      if (this.timelineData.trySetActiveTrace(event.newFocusedView.traces[0])) {
195        await this.timelineComponent?.onWinscopeEvent(
196          new ActiveTraceChanged(event.newFocusedView.traces[0]),
197        );
198      }
199      this.focusedTabView = event.newFocusedView;
200      await this.propagateTracePosition(
201        this.timelineData.getCurrentPosition(),
202        false,
203      );
204    });
205
206    await event.visit(
207      WinscopeEventType.TRACE_POSITION_UPDATE,
208      async (event) => {
209        if (event.updateTimeline) {
210          this.timelineData.setPosition(event.position);
211        }
212        await this.propagateTracePosition(event.position, false);
213      },
214    );
215
216    await event.visit(
217      WinscopeEventType.EXPANDED_TIMELINE_TOGGLED,
218      async (event) => {
219        await this.propagateToOverlays(event);
220      },
221    );
222
223    await event.visit(WinscopeEventType.ACTIVE_TRACE_CHANGED, async (event) => {
224      this.timelineData.trySetActiveTrace(event.trace);
225      await this.timelineComponent?.onWinscopeEvent(event);
226    });
227
228    await event.visit(WinscopeEventType.DARK_MODE_TOGGLED, async (event) => {
229      await this.timelineComponent?.onWinscopeEvent(event);
230    });
231  }
232
233  private async loadFiles(files: File[], source: FilesSource) {
234    const warnings: UserWarning[] = [];
235    const notificationsListener: UserNotificationsListener = {
236      onNotifications(notifications: UserWarning[]) {
237        warnings.push(...notifications);
238      },
239    };
240    await this.tracePipeline.loadFiles(
241      files,
242      source,
243      notificationsListener,
244      this.currentProgressListener,
245    );
246
247    if (warnings.length > 0) {
248      this.userNotificationsListener.onNotifications(warnings);
249    }
250  }
251
252  private async propagateTracePosition(
253    position: TracePosition | undefined,
254    omitCrossToolProtocol: boolean,
255  ) {
256    if (!position) {
257      return;
258    }
259
260    const event = new TracePositionUpdate(position);
261    const receivers: WinscopeEventListener[] = [...this.viewers].filter(
262      (viewer) => this.isViewerVisible(viewer),
263    );
264    if (this.timelineComponent) {
265      receivers.push(this.timelineComponent);
266    }
267
268    const promises = receivers.map((receiver) => {
269      return receiver.onWinscopeEvent(event);
270    });
271
272    if (!omitCrossToolProtocol) {
273      const event = new TracePositionUpdate(position);
274      promises.push(this.crossToolProtocol.onWinscopeEvent(event));
275    }
276
277    await Promise.all(promises);
278  }
279
280  private isViewerVisible(viewer: Viewer): boolean {
281    if (!this.focusedTabView) {
282      // During initialization no tab is focused.
283      // Let's just consider all viewers as visible and to be updated.
284      return true;
285    }
286
287    return viewer.getViews().some((view) => {
288      if (view === this.focusedTabView) {
289        return true;
290      }
291      if (view.type === ViewType.OVERLAY) {
292        // Nice to have: update viewer only if overlay view is actually visible (not minimized)
293        return true;
294      }
295      return false;
296    });
297  }
298
299  private async processRemoteToolDeferredTimestampReceived(
300    deferredTimestamp: () => Timestamp | undefined,
301  ) {
302    this.lastRemoteToolDeferredTimestampReceived = deferredTimestamp;
303
304    if (!this.areViewersLoaded) {
305      return; // apply timestamp later when traces are visualized
306    }
307
308    const timestamp = deferredTimestamp();
309    if (!timestamp) {
310      return;
311    }
312
313    const position = this.timelineData.makePositionFromActiveTrace(timestamp);
314    this.timelineData.setPosition(position);
315
316    await this.propagateTracePosition(
317      this.timelineData.getCurrentPosition(),
318      true,
319    );
320  }
321
322  private async processRemoteFilesReceived(files: File[], source: FilesSource) {
323    await this.resetAppToInitialState();
324    this.currentProgressListener = this.uploadTracesComponent;
325    await this.loadFiles(files, source);
326  }
327
328  private async loadViewers() {
329    this.currentProgressListener?.onProgressUpdate(
330      'Computing frame mapping...',
331      undefined,
332    );
333
334    // TODO: move this into the ProgressListener
335    // allow the UI to update before making the main thread very busy
336    await TimeUtils.sleepMs(10);
337
338    this.tracePipeline.filterTracesWithoutVisualization();
339    await this.tracePipeline.buildTraces();
340    this.currentProgressListener?.onOperationFinished();
341
342    this.currentProgressListener?.onProgressUpdate(
343      'Initializing UI...',
344      undefined,
345    );
346
347    // TODO: move this into the ProgressListener
348    // allow the UI to update before making the main thread very busy
349    await TimeUtils.sleepMs(10);
350
351    await this.timelineData.initialize(
352      this.tracePipeline.getTraces(),
353      await this.tracePipeline.getScreenRecordingVideo(),
354      this.tracePipeline.getTimestampConverter(),
355    );
356
357    this.viewers = new ViewerFactory().createViewers(
358      this.tracePipeline.getTraces(),
359      this.storage,
360    );
361    this.viewers.forEach((viewer) =>
362      viewer.setEmitEvent(async (event) => {
363        await this.onWinscopeEvent(event);
364      }),
365    );
366
367    // Set initial trace position as soon as UI is created
368    const initialPosition = this.getInitialTracePosition();
369    this.timelineData.setPosition(initialPosition);
370
371    // Make sure all viewers are initialized and have performed the heavy pre-processing they need
372    // at this stage, while the "initializing UI" progress message is still being displayed.
373    // The viewers initialization is triggered by sending them a "trace position update".
374    await this.propagateTracePosition(initialPosition, true);
375
376    this.focusedTabView = this.viewers
377      .find((v) => v.getViews()[0].type !== ViewType.OVERLAY)
378      ?.getViews()[0];
379    this.areViewersLoaded = true;
380
381    // Notify app component (i.e. render viewers), only after all viewers have been initialized
382    // (see above).
383    //
384    // Notifying the app component first could result in this kind of interleaved execution:
385    // 1. Mediator notifies app component
386    //    1.1. App component renders UI components
387    //    1.2. Mediator receives back a "view switched" event
388    //    1.2. Mediator sends "trace position update" to viewers
389    // 2. Mediator sends "trace position update" to viewers to initialize them (see above)
390    //
391    // and because our data load operations are async and involve task suspensions, the two
392    // "trace position update" could be processed concurrently within the same viewer.
393    // Meaning the viewer could perform twice the initial heavy pre-processing,
394    // thus increasing UI initialization times.
395    await this.appComponent.onWinscopeEvent(new ViewersLoaded(this.viewers));
396  }
397
398  private getInitialTracePosition(): TracePosition | undefined {
399    if (this.lastRemoteToolDeferredTimestampReceived) {
400      const lastRemoteToolTimestamp =
401        this.lastRemoteToolDeferredTimestampReceived();
402      if (lastRemoteToolTimestamp) {
403        return this.timelineData.makePositionFromActiveTrace(
404          lastRemoteToolTimestamp,
405        );
406      }
407    }
408
409    const position = this.timelineData.getCurrentPosition();
410    if (position) {
411      return position;
412    }
413
414    // TimelineData might not provide a TracePosition because all the loaded traces are
415    // dumps with invalid timestamps (value zero). In this case let's create a TracePosition
416    // out of any entry from the loaded traces (if available).
417    const firstEntries = this.tracePipeline
418      .getTraces()
419      .mapTrace((trace) => {
420        if (trace.lengthEntries > 0) {
421          return trace.getEntry(0);
422        }
423        return undefined;
424      })
425      .filter((entry) => {
426        return entry !== undefined;
427      }) as Array<TraceEntry<object>>;
428
429    if (firstEntries.length > 0) {
430      return TracePosition.fromTraceEntry(firstEntries[0]);
431    }
432
433    return undefined;
434  }
435
436  private async resetAppToInitialState() {
437    this.tracePipeline.clear();
438    this.timelineData.clear();
439    this.viewers = [];
440    this.areViewersLoaded = false;
441    this.lastRemoteToolDeferredTimestampReceived = undefined;
442    this.focusedTabView = undefined;
443    await this.appComponent.onWinscopeEvent(new ViewersUnloaded());
444  }
445
446  private async propagateToOverlays(event: ExpandedTimelineToggled) {
447    const overlayViewers = this.viewers.filter((viewer) =>
448      viewer.getViews().some((view) => view.type === ViewType.OVERLAY),
449    );
450    for (const overlay of overlayViewers) {
451      await overlay.onWinscopeEvent(event);
452    }
453  }
454}
455