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