• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2024 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 {DOMUtils} from 'common/dom_utils';
18import {FunctionUtils} from 'common/function_utils';
19import {Timestamp} from 'common/time/time';
20import {Analytics} from 'logging/analytics';
21import {
22  TracePositionUpdate,
23  WinscopeEvent,
24  WinscopeEventType,
25} from 'messaging/winscope_event';
26import {EmitEvent} from 'messaging/winscope_event_emitter';
27import {Trace, TraceEntry} from 'trace/trace';
28import {TraceEntryFinder} from 'trace/trace_entry_finder';
29import {TRACE_INFO} from 'trace/trace_info';
30import {TracePosition} from 'trace/trace_position';
31import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
32import {PropertiesPresenter} from 'viewers/common/properties_presenter';
33import {TextFilter} from 'viewers/common/text_filter';
34import {UserOptions} from 'viewers/common/user_options';
35import {LogPresenter} from './log_presenter';
36import {LogEntry, LogHeader, UiDataLog} from './ui_data_log';
37import {
38  LogFilterChangeDetail,
39  LogTextFilterChangeDetail,
40  TimestampClickDetail,
41  ViewerEvents,
42} from './viewer_events';
43
44export type NotifyLogViewCallbackType<UiData> = (uiData: UiData) => void;
45
46export abstract class AbstractLogViewerPresenter<
47  UiData extends UiDataLog,
48  TraceEntryType extends object,
49> {
50  protected static readonly VALUE_NA = 'N/A';
51  protected emitAppEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
52  protected abstract logPresenter: LogPresenter<LogEntry>;
53  protected propertiesPresenter?: PropertiesPresenter;
54  protected keepCalculated?: boolean;
55  private activeTrace?: Trace<object>;
56  private isInitialized = false;
57
58  protected constructor(
59    protected readonly trace: Trace<TraceEntryType>,
60    private readonly notifyViewCallback: NotifyLogViewCallbackType<UiData>,
61    protected readonly uiData: UiData,
62  ) {
63    this.notifyViewChanged();
64  }
65
66  setEmitEvent(callback: EmitEvent) {
67    this.emitAppEvent = callback;
68  }
69
70  addEventListeners(htmlElement: HTMLElement) {
71    htmlElement.addEventListener(
72      ViewerEvents.LogFilterChange,
73      async (event) => {
74        const detail: LogFilterChangeDetail = (event as CustomEvent).detail;
75        await this.onSelectFilterChange(detail.header, detail.value);
76      },
77    );
78    htmlElement.addEventListener(
79      ViewerEvents.LogTextFilterChange,
80      async (event) => {
81        const detail: LogTextFilterChangeDetail = (event as CustomEvent).detail;
82        await this.onTextFilterChange(detail.header, detail.filter);
83      },
84    );
85    htmlElement.addEventListener(ViewerEvents.LogEntryClick, async (event) => {
86      await this.onLogEntryClick((event as CustomEvent).detail);
87    });
88    htmlElement.addEventListener(
89      ViewerEvents.ArrowDownPress,
90      async (event) => await this.onArrowDownPress(),
91    );
92    htmlElement.addEventListener(
93      ViewerEvents.ArrowUpPress,
94      async (event) => await this.onArrowUpPress(),
95    );
96    htmlElement.addEventListener(ViewerEvents.TimestampClick, async (event) => {
97      const detail: TimestampClickDetail = (event as CustomEvent).detail;
98      if (detail.entry !== undefined) {
99        await this.onLogTimestampClick(detail.entry);
100      } else if (detail.timestamp !== undefined) {
101        await this.onRawTimestampClick(detail.timestamp);
102      }
103    });
104    htmlElement.addEventListener(
105      ViewerEvents.PropertiesUserOptionsChange,
106      (event) =>
107        this.onPropertiesUserOptionsChange(
108          (event as CustomEvent).detail.userOptions,
109        ),
110    );
111    htmlElement.addEventListener(
112      ViewerEvents.PropertiesFilterChange,
113      async (event) => {
114        const detail: TextFilter = (event as CustomEvent).detail;
115        await this.onPropertiesFilterChange(detail);
116      },
117    );
118
119    document.addEventListener('keydown', async (event: KeyboardEvent) => {
120      const isViewerVisible = DOMUtils.isElementVisible(htmlElement);
121      const isPositionChange =
122        event.key === 'ArrowRight' || event.key === 'ArrowLeft';
123      if (!isViewerVisible || !isPositionChange) {
124        return;
125      }
126      event.preventDefault();
127      await this.onPositionChangeByKeyPress(event);
128    });
129
130    this.addViewerSpecificListeners(htmlElement);
131  }
132
133  async onAppEvent(event: WinscopeEvent) {
134    await event.visit(
135      WinscopeEventType.TRACE_POSITION_UPDATE,
136      async (event) => {
137        if (this.uiData.isFetchingData) {
138          return;
139        }
140        if (!this.isInitialized) {
141          this.uiData.isFetchingData = true;
142          this.notifyViewChanged();
143          if (this.initializeTraceSpecificData) {
144            await this.initializeTraceSpecificData();
145          }
146          this.makeUiData().then(async () => {
147            await this.applyTracePositionUpdate(event);
148            this.uiData.isFetchingData = false;
149            this.notifyViewChanged();
150            this.isInitialized = true;
151          });
152        } else {
153          await this.applyTracePositionUpdate(event);
154        }
155      },
156    );
157    await event.visit(WinscopeEventType.DARK_MODE_TOGGLED, async (event) => {
158      this.uiData.isDarkMode = event.isDarkMode;
159      this.notifyViewChanged();
160    });
161    await event.visit(WinscopeEventType.ACTIVE_TRACE_CHANGED, async (event) => {
162      this.activeTrace = event.trace;
163    });
164  }
165
166  async onSelectFilterChange(header: LogHeader, value: string[]) {
167    this.logPresenter.applySelectFilterChange(header, value);
168    await this.updatePropertiesTree();
169    this.uiData.currentIndex = this.logPresenter.getCurrentIndex();
170    this.uiData.selectedIndex = this.logPresenter.getSelectedIndex();
171    this.uiData.scrollToIndex = this.logPresenter.getScrollToIndex();
172    this.uiData.entries = this.logPresenter.getFilteredEntries();
173    this.notifyViewChanged();
174  }
175
176  async onTextFilterChange(header: LogHeader, value: TextFilter) {
177    this.logPresenter.applyTextFilterChange(header, value);
178    await this.updatePropertiesTree();
179    this.uiData.currentIndex = this.logPresenter.getCurrentIndex();
180    this.uiData.selectedIndex = this.logPresenter.getSelectedIndex();
181    this.uiData.scrollToIndex = this.logPresenter.getScrollToIndex();
182    this.uiData.entries = this.logPresenter.getFilteredEntries();
183    this.notifyViewChanged();
184  }
185
186  async onPropertiesUserOptionsChange(userOptions: UserOptions) {
187    if (!this.propertiesPresenter) {
188      return;
189    }
190    this.propertiesPresenter.applyPropertiesUserOptionsChange(userOptions);
191    this.uiData.propertiesUserOptions =
192      this.propertiesPresenter.getUserOptions();
193    await this.updatePropertiesTree(false);
194    this.notifyViewChanged();
195  }
196
197  async onPropertiesFilterChange(textFilter: TextFilter) {
198    if (!this.propertiesPresenter) {
199      return;
200    }
201    this.propertiesPresenter.applyPropertiesFilterChange(textFilter);
202    await this.updatePropertiesTree(false);
203    this.uiData.propertiesFilter = textFilter;
204    this.notifyViewChanged();
205  }
206
207  async onLogTimestampClick(traceEntry: TraceEntry<object>) {
208    await this.emitAppEvent(
209      TracePositionUpdate.fromTraceEntry(traceEntry, true),
210    );
211  }
212
213  async onRawTimestampClick(timestamp: Timestamp) {
214    await this.emitAppEvent(TracePositionUpdate.fromTimestamp(timestamp, true));
215  }
216
217  async onLogEntryClick(index: number) {
218    this.logPresenter.applyLogEntryClick(index);
219    this.updateIndicesUiData();
220    await this.updatePropertiesTree();
221    this.notifyViewChanged();
222  }
223
224  async onArrowDownPress() {
225    this.logPresenter.applyArrowDownPress();
226    this.updateIndicesUiData();
227    await this.updatePropertiesTree();
228    this.notifyViewChanged();
229  }
230
231  async onArrowUpPress() {
232    this.logPresenter.applyArrowUpPress();
233    this.updateIndicesUiData();
234    await this.updatePropertiesTree();
235    this.notifyViewChanged();
236  }
237
238  async onPositionChangeByKeyPress(event: KeyboardEvent) {
239    const currIndex = this.uiData.currentIndex;
240    if (this.activeTrace === this.trace && currIndex !== undefined) {
241      if (event.key === 'ArrowRight') {
242        event.stopImmediatePropagation();
243        if (currIndex < this.uiData.entries.length - 1) {
244          const currTimestamp =
245            this.uiData.entries[currIndex].traceEntry.getTimestamp();
246          const nextEntry = this.uiData.entries
247            .slice(currIndex + 1)
248            .find((entry) => entry.traceEntry.getTimestamp() > currTimestamp);
249          if (nextEntry) {
250            return this.emitAppEvent(
251              new TracePositionUpdate(
252                TracePosition.fromTraceEntry(nextEntry.traceEntry),
253                true,
254              ),
255            );
256          }
257        }
258      } else {
259        event.stopImmediatePropagation();
260        if (currIndex > 0) {
261          let prev = currIndex - 1;
262          while (prev >= 0) {
263            const prevEntry = this.uiData.entries[prev].traceEntry;
264            if (prevEntry.hasValidTimestamp()) {
265              return this.emitAppEvent(
266                new TracePositionUpdate(
267                  TracePosition.fromTraceEntry(prevEntry),
268                  true,
269                ),
270              );
271            }
272            prev--;
273          }
274        }
275      }
276    }
277  }
278
279  protected addViewerSpecificListeners(htmlElement: HTMLElement) {
280    // do nothing
281  }
282
283  protected refreshUiData() {
284    this.uiData.headers = this.logPresenter.getHeaders();
285    this.uiData.entries = this.logPresenter.getFilteredEntries();
286    this.uiData.selectedIndex = this.logPresenter.getSelectedIndex();
287    this.uiData.scrollToIndex = this.logPresenter.getScrollToIndex();
288    this.uiData.currentIndex = this.logPresenter.getCurrentIndex();
289    if (this.propertiesPresenter) {
290      this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree();
291      this.uiData.propertiesUserOptions =
292        this.propertiesPresenter.getUserOptions();
293      this.uiData.propertiesFilter = this.propertiesPresenter.getTextFilter();
294    }
295  }
296
297  private async applyTracePositionUpdate(event: TracePositionUpdate) {
298    let entry: TraceEntry<TraceEntryType> | undefined;
299    if (event.position.entry?.getFullTrace() === this.trace) {
300      entry = event.position.entry as TraceEntry<TraceEntryType>;
301    } else {
302      entry = TraceEntryFinder.findCorrespondingEntry(
303        this.trace,
304        event.position,
305      );
306    }
307    this.logPresenter.applyTracePositionUpdate(entry);
308
309    this.uiData.selectedIndex = this.logPresenter.getSelectedIndex();
310    this.uiData.scrollToIndex = this.logPresenter.getScrollToIndex();
311    this.uiData.currentIndex = this.logPresenter.getCurrentIndex();
312
313    if (this.propertiesPresenter) {
314      await this.updatePropertiesTree();
315      this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree();
316    }
317
318    this.notifyViewChanged();
319  }
320
321  protected async updatePropertiesTree(updateDefaultAllowlist = true) {
322    if (this.propertiesPresenter) {
323      const traceName = TRACE_INFO[this.trace.type].name;
324      const propertiesStartTime = Date.now();
325
326      const tree = this.getPropertiesTree();
327      this.propertiesPresenter.setPropertiesTree(tree);
328      if (updateDefaultAllowlist && this.updateDefaultAllowlist) {
329        this.updateDefaultAllowlist(tree);
330      }
331      await this.propertiesPresenter.formatPropertiesTree(
332        undefined,
333        undefined,
334        this.keepCalculated ?? false,
335        this.trace.type,
336      );
337      this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree();
338      Analytics.Navigation.logFetchComponentDataTime(
339        'properties',
340        traceName,
341        false,
342        Date.now() - propertiesStartTime,
343      );
344    }
345  }
346
347  private async makeUiData() {
348    const headers = this.makeHeaders();
349    const allEntries = await this.makeUiDataEntries(headers);
350    if (this.updateFiltersInHeaders) {
351      this.updateFiltersInHeaders(headers, allEntries);
352    }
353    this.logPresenter.setAllEntries(allEntries);
354    this.logPresenter.setHeaders(headers);
355    this.refreshUiData();
356  }
357
358  private updateIndicesUiData() {
359    this.uiData.selectedIndex = this.logPresenter.getSelectedIndex();
360    this.uiData.currentIndex = this.logPresenter.getCurrentIndex();
361    this.uiData.scrollToIndex = this.logPresenter.getScrollToIndex();
362  }
363
364  private getPropertiesTree(): PropertyTreeNode | undefined {
365    const entries = this.logPresenter.getFilteredEntries();
366    const selectedIndex = this.logPresenter.getSelectedIndex();
367    const currentIndex = this.logPresenter.getCurrentIndex();
368    if (selectedIndex !== undefined) {
369      return entries.at(selectedIndex)?.propertiesTree;
370    }
371    if (currentIndex !== undefined) {
372      return entries.at(currentIndex)?.propertiesTree;
373    }
374    return undefined;
375  }
376
377  protected notifyViewChanged() {
378    this.notifyViewCallback(this.uiData);
379  }
380
381  protected abstract makeHeaders(): LogHeader[];
382  protected abstract makeUiDataEntries(
383    headers: LogHeader[],
384  ): Promise<LogEntry[]>;
385  protected initializeTraceSpecificData?(): Promise<void>;
386  protected updateFiltersInHeaders?(
387    headers: LogHeader[],
388    allEntries: LogEntry[],
389  ): void;
390  protected updateDefaultAllowlist?(tree: PropertyTreeNode | undefined): void;
391}
392