xref: /aosp_15_r20/development/tools/winscope/src/viewers/viewer_input/presenter.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1/*
2 * Copyright 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 {assertDefined} from 'common/assert_utils';
18import {PersistentStoreProxy} from 'common/persistent_store_proxy';
19import {Store} from 'common/store';
20import {TabbedViewSwitchRequest} from 'messaging/winscope_event';
21import {CustomQueryType} from 'trace/custom_query';
22import {Trace, TraceEntry, TraceEntryLazy} from 'trace/trace';
23import {Traces} from 'trace/traces';
24import {TraceType} from 'trace/trace_type';
25import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
26import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
27import {
28  AbstractLogViewerPresenter,
29  NotifyLogViewCallbackType,
30} from 'viewers/common/abstract_log_viewer_presenter';
31import {VISIBLE_CHIP} from 'viewers/common/chip';
32import {LogSelectFilter} from 'viewers/common/log_filters';
33import {LogPresenter} from 'viewers/common/log_presenter';
34import {PropertiesPresenter} from 'viewers/common/properties_presenter';
35import {RectsPresenter} from 'viewers/common/rects_presenter';
36import {TextFilter} from 'viewers/common/text_filter';
37import {LogHeader} from 'viewers/common/ui_data_log';
38import {UI_RECT_FACTORY} from 'viewers/common/ui_rect_factory';
39import {UserOptions} from 'viewers/common/user_options';
40import {ViewerEvents} from 'viewers/common/viewer_events';
41import {
42  convertRectIdToLayerorDisplayName,
43  makeDisplayIdentifiers,
44} from 'viewers/viewer_surface_flinger/presenter';
45import {DispatchEntryFormatter} from './operations/dispatch_entry_formatter';
46import {InputEntry, UiData} from './ui_data';
47
48enum InputEventType {
49  KEY,
50  MOTION,
51}
52
53export class Presenter extends AbstractLogViewerPresenter<
54  UiData,
55  PropertyTreeNode
56> {
57  private static readonly COLUMNS = {
58    type: {
59      name: 'Type',
60      cssClass: 'input-type inline',
61    },
62    source: {
63      name: 'Source',
64      cssClass: 'input-source',
65    },
66    action: {
67      name: 'Action',
68      cssClass: 'input-action',
69    },
70    deviceId: {
71      name: 'Device',
72      cssClass: 'input-device-id right-align',
73    },
74    displayId: {
75      name: 'Display',
76      cssClass: 'input-display-id right-align',
77    },
78    details: {
79      name: 'Details',
80      cssClass: 'input-details',
81    },
82    dispatchWindows: {
83      name: 'Target Windows',
84      cssClass: 'input-windows',
85    },
86  };
87  static readonly DENYLIST_DISPATCH_PROPERTIES = ['eventId'];
88
89  private readonly traces: Traces;
90  private readonly surfaceFlingerTrace: Trace<HierarchyTreeNode> | undefined;
91  protected override uiData: UiData = UiData.createEmpty();
92  private allEntries: InputEntry[] | undefined;
93
94  private readonly layerIdToName = new Map<number, string>();
95  private readonly allInputLayerIds = new Set<number>();
96
97  protected override logPresenter = new LogPresenter<InputEntry>();
98  protected override propertiesPresenter = new PropertiesPresenter(
99    {},
100    new TextFilter(),
101    [],
102  );
103  protected dispatchPropertiesPresenter = new PropertiesPresenter(
104    {},
105    new TextFilter(),
106    Presenter.DENYLIST_DISPATCH_PROPERTIES,
107    [new DispatchEntryFormatter(this.layerIdToName)],
108  );
109  private readonly currentTargetWindowIds = new Set<string>();
110
111  private readonly rectsPresenter = new RectsPresenter(
112    PersistentStoreProxy.new<UserOptions>(
113      'InputWindowRectsOptions',
114      {
115        showOnlyWithContent: {
116          name: 'Has input',
117          icon: 'pan_tool_alt',
118          enabled: false,
119        },
120        showOnlyVisible: {
121          name: 'Show only',
122          chip: VISIBLE_CHIP,
123          enabled: true,
124        },
125      },
126      this.storage,
127    ),
128    (tree: HierarchyTreeNode) =>
129      UI_RECT_FACTORY.makeInputRects(tree, (id) =>
130        this.currentTargetWindowIds.has(id),
131      ),
132    makeDisplayIdentifiers,
133    convertRectIdToLayerorDisplayName,
134  );
135
136  constructor(
137    traces: Traces,
138    mergedInputEventTrace: Trace<PropertyTreeNode>,
139    private readonly storage: Store,
140    readonly notifyInputViewCallback: NotifyLogViewCallbackType<UiData>,
141  ) {
142    const uiData = UiData.createEmpty();
143    uiData.isDarkMode = storage.get('dark-mode') === 'true';
144    super(
145      mergedInputEventTrace,
146      (uiData) => notifyInputViewCallback(uiData as UiData),
147      uiData,
148    );
149    this.traces = traces;
150    this.surfaceFlingerTrace = this.traces.getTrace(TraceType.SURFACE_FLINGER);
151  }
152
153  async onDispatchPropertiesFilterChange(textFilter: TextFilter) {
154    this.dispatchPropertiesPresenter.applyPropertiesFilterChange(textFilter);
155    await this.updateDispatchPropertiesTree();
156    this.uiData.dispatchPropertiesFilter = textFilter;
157    this.notifyViewChanged();
158  }
159
160  protected override async initializeTraceSpecificData() {
161    if (this.surfaceFlingerTrace !== undefined) {
162      const layerMappings = await this.surfaceFlingerTrace.customQuery(
163        CustomQueryType.SF_LAYERS_ID_AND_NAME,
164      );
165      layerMappings.forEach(({id, name}) => this.layerIdToName.set(id, name));
166    }
167  }
168
169  protected override makeHeaders(): LogHeader[] {
170    return [
171      new LogHeader(Presenter.COLUMNS.type),
172      new LogHeader(Presenter.COLUMNS.source),
173      new LogHeader(Presenter.COLUMNS.action),
174      new LogHeader(Presenter.COLUMNS.deviceId),
175      new LogHeader(Presenter.COLUMNS.displayId),
176      new LogHeader(Presenter.COLUMNS.details),
177      new LogHeader(
178        Presenter.COLUMNS.dispatchWindows,
179        new LogSelectFilter([], true, '300', '300px'),
180      ),
181    ];
182  }
183
184  protected override async makeUiDataEntries(): Promise<InputEntry[]> {
185    const entries: InputEntry[] = [];
186    for (let i = 0; i < this.trace.lengthEntries; i++) {
187      const traceEntry = assertDefined(this.trace.getEntry(i));
188      const entry = await this.makeInputEntry(traceEntry);
189      entries.push(entry);
190    }
191    return Promise.resolve(entries);
192  }
193
194  protected override updateFiltersInHeaders(headers: LogHeader[]) {
195    const dispatchWindowsHeader = headers.find(
196      (header) => header.spec === Presenter.COLUMNS.dispatchWindows,
197    );
198    (assertDefined(dispatchWindowsHeader?.filter) as LogSelectFilter).options =
199      [...this.allInputLayerIds.values()].map((layerId) => {
200        return this.getLayerDisplayName(layerId);
201      });
202  }
203
204  private async makeInputEntry(
205    traceEntry: TraceEntryLazy<PropertyTreeNode>,
206  ): Promise<InputEntry> {
207    const wrapperTree = await traceEntry.getValue();
208
209    let eventTree = wrapperTree.getChildByName('keyEvent');
210    let type = InputEventType.KEY;
211    if (eventTree === undefined || eventTree.getAllChildren().length === 0) {
212      eventTree = assertDefined(wrapperTree.getChildByName('motionEvent'));
213      type = InputEventType.MOTION;
214    }
215    eventTree.setIsRoot(true);
216
217    const dispatchTree = assertDefined(
218      wrapperTree.getChildByName('windowDispatchEvents'),
219    );
220    dispatchTree.setIsRoot(true);
221    dispatchTree.getAllChildren().forEach((dispatchEntry) => {
222      const windowIdNode = dispatchEntry.getChildByName('windowId');
223      const windowId = Number(windowIdNode?.getValue() ?? -1);
224      this.allInputLayerIds.add(windowId);
225    });
226
227    let sfEntry: TraceEntry<HierarchyTreeNode> | undefined;
228    if (this.surfaceFlingerTrace !== undefined && this.trace.hasFrameInfo()) {
229      const frame = traceEntry.getFramesRange()?.start;
230      if (frame !== undefined) {
231        const sfFrame = this.surfaceFlingerTrace.getFrame(frame);
232        if (sfFrame.lengthEntries > 0) {
233          sfEntry = sfFrame.getEntry(0);
234        }
235      }
236    }
237
238    return new InputEntry(
239      traceEntry,
240      [
241        {
242          spec: Presenter.COLUMNS.type,
243          value: type === InputEventType.KEY ? 'KEY' : 'MOTION',
244          propagateEntryTimestamp: true,
245        },
246        {
247          spec: Presenter.COLUMNS.source,
248          value: assertDefined(eventTree.getChildByName('source'))
249            .formattedValue()
250            .replace('SOURCE_', ''),
251        },
252        {
253          spec: Presenter.COLUMNS.action,
254          value: assertDefined(eventTree.getChildByName('action'))
255            .formattedValue()
256            .replace('ACTION_', ''),
257        },
258        {
259          spec: Presenter.COLUMNS.deviceId,
260          value: assertDefined(eventTree.getChildByName('deviceId')).getValue(),
261        },
262        {
263          spec: Presenter.COLUMNS.displayId,
264          value: assertDefined(
265            eventTree.getChildByName('displayId'),
266          ).getValue(),
267        },
268        {
269          spec: Presenter.COLUMNS.details,
270          value:
271            type === InputEventType.KEY
272              ? Presenter.extractKeyDetails(eventTree, dispatchTree)
273              : Presenter.extractDispatchDetails(dispatchTree),
274        },
275        {
276          spec: Presenter.COLUMNS.dispatchWindows,
277          value: dispatchTree
278            .getAllChildren()
279            .map((dispatchEntry) => {
280              const windowId = Number(
281                dispatchEntry.getChildByName('windowId')?.getValue() ?? -1,
282              );
283              return this.getLayerDisplayName(windowId);
284            })
285            .join(', '),
286        },
287      ],
288      eventTree,
289      dispatchTree,
290      sfEntry,
291    );
292  }
293
294  private getLayerDisplayName(layerId: number): string {
295    // Surround the name using the invisible zero-width non-joiner character to ensure
296    // the full string is matched while filtering.
297    return `\u{200C}${
298      this.layerIdToName.get(layerId) ?? layerId.toString()
299    }\u{200C}`;
300  }
301
302  private static extractKeyDetails(
303    eventTree: PropertyTreeNode,
304    dispatchTree: PropertyTreeNode,
305  ): string {
306    const keyDetails =
307      'Keycode: ' +
308        eventTree
309          .getChildByName('keyCode')
310          ?.formattedValue()
311          ?.replace(/^KEYCODE_/, '') ?? '<?>';
312    return keyDetails + ' ' + Presenter.extractDispatchDetails(dispatchTree);
313  }
314
315  private static extractDispatchDetails(
316    dispatchTree: PropertyTreeNode,
317  ): string {
318    let details = '';
319    dispatchTree.getAllChildren().forEach((dispatchEntry) => {
320      const windowIdNode = dispatchEntry.getChildByName('windowId');
321      if (windowIdNode === undefined) {
322        return;
323      }
324      if (windowIdNode.formattedValue() === '0') {
325        // Skip showing windowId 0, which is an omnipresent system window.
326        return;
327      }
328      details += windowIdNode.getValue() + ', ';
329    });
330    return '[' + details.slice(0, -2) + ']';
331  }
332
333  protected override async updatePropertiesTree() {
334    await super.updatePropertiesTree();
335    await this.updateDispatchPropertiesTree();
336    await this.updateRects();
337  }
338
339  private async updateDispatchPropertiesTree() {
340    const inputEntry = this.getCurrentEntry();
341    const tree = inputEntry?.dispatchPropertiesTree;
342    this.dispatchPropertiesPresenter.setPropertiesTree(tree);
343    await this.dispatchPropertiesPresenter.formatPropertiesTree(
344      undefined,
345      undefined,
346      this.keepCalculated ?? false,
347    );
348    this.uiData.dispatchPropertiesTree =
349      this.dispatchPropertiesPresenter.getFormattedTree();
350  }
351
352  private async updateRects() {
353    if (this.surfaceFlingerTrace === undefined) {
354      return;
355    }
356    const inputEntry = this.getCurrentEntry();
357
358    this.currentTargetWindowIds.clear();
359    inputEntry?.dispatchPropertiesTree
360      ?.getAllChildren()
361      ?.forEach((dispatchEntry) => {
362        const windowId = dispatchEntry.getChildByName('windowId');
363        if (windowId !== undefined) {
364          this.currentTargetWindowIds.add(`${Number(windowId.getValue())}`);
365        }
366      });
367
368    if (inputEntry?.surfaceFlingerEntry !== undefined) {
369      const node = await inputEntry.surfaceFlingerEntry.getValue();
370      this.rectsPresenter.applyHierarchyTreesChange([
371        [this.surfaceFlingerTrace, [node]],
372      ]);
373      this.uiData.rectsToDraw = this.rectsPresenter.getRectsToDraw();
374      this.uiData.rectIdToShowState =
375        this.rectsPresenter.getRectIdToShowState();
376    } else {
377      this.uiData.rectsToDraw = [];
378      this.uiData.rectIdToShowState = undefined;
379    }
380    this.uiData.rectsUserOptions = this.rectsPresenter.getUserOptions();
381    this.uiData.displays = this.rectsPresenter.getDisplays();
382  }
383
384  private getCurrentEntry(): InputEntry | undefined {
385    const entries = this.logPresenter.getFilteredEntries();
386    const selectedIndex = this.logPresenter.getSelectedIndex();
387    const currentIndex = this.logPresenter.getCurrentIndex();
388    const index = selectedIndex ?? currentIndex;
389    if (index === undefined) {
390      return undefined;
391    }
392    return entries[index];
393  }
394
395  override addEventListeners(htmlElement: HTMLElement) {
396    super.addEventListeners(htmlElement);
397
398    htmlElement.addEventListener(
399      ViewerEvents.HighlightedPropertyChange,
400      (event) =>
401        this.onHighlightedPropertyChange((event as CustomEvent).detail.id),
402    );
403
404    htmlElement.addEventListener(ViewerEvents.HighlightedIdChange, (event) =>
405      this.onHighlightedIdChange((event as CustomEvent).detail.id),
406    );
407
408    htmlElement.addEventListener(
409      ViewerEvents.RectsUserOptionsChange,
410      async (event) => {
411        await this.onRectsUserOptionsChange(
412          (event as CustomEvent).detail.userOptions,
413        );
414      },
415    );
416
417    htmlElement.addEventListener(ViewerEvents.RectsDblClick, async (event) => {
418      await this.onRectDoubleClick();
419    });
420
421    htmlElement.addEventListener(
422      ViewerEvents.DispatchPropertiesFilterChange,
423      async (event) => {
424        const detail: TextFilter = (event as CustomEvent).detail;
425        await this.onDispatchPropertiesFilterChange(detail);
426      },
427    );
428  }
429
430  onHighlightedPropertyChange(id: string) {
431    this.propertiesPresenter.applyHighlightedPropertyChange(id);
432    this.dispatchPropertiesPresenter.applyHighlightedPropertyChange(id);
433    this.uiData.highlightedProperty =
434      id === this.uiData.highlightedProperty ? '' : id;
435    this.notifyViewChanged();
436  }
437
438  async onHighlightedIdChange(id: string) {
439    this.uiData.highlightedRect = id === this.uiData.highlightedRect ? '' : id;
440    await this.updateRects();
441    this.notifyViewChanged();
442  }
443
444  async onRectsUserOptionsChange(userOptions: UserOptions) {
445    this.rectsPresenter.applyRectsUserOptionsChange(userOptions);
446    await this.updateRects();
447    this.notifyViewChanged();
448  }
449
450  async onRectDoubleClick() {
451    await this.emitAppEvent(
452      new TabbedViewSwitchRequest(assertDefined(this.surfaceFlingerTrace)),
453    );
454  }
455}
456