xref: /aosp_15_r20/development/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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} from 'common/assert_utils';
18import {PersistentStoreProxy} from 'common/persistent_store_proxy';
19import {Store} from 'common/store';
20import {
21  TabbedViewSwitchRequest,
22  TracePositionUpdate,
23} from 'messaging/winscope_event';
24import {LayerFlag} from 'parsers/surface_flinger/layer_flag';
25import {CustomQueryType} from 'trace/custom_query';
26import {Trace} from 'trace/trace';
27import {Traces} from 'trace/traces';
28import {TraceEntryFinder} from 'trace/trace_entry_finder';
29import {TraceType} from 'trace/trace_type';
30import {
31  EMPTY_OBJ_STRING,
32  FixedStringFormatter,
33} from 'trace/tree_node/formatters';
34import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
35import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
36import {
37  AbstractHierarchyViewerPresenter,
38  NotifyHierarchyViewCallbackType,
39} from 'viewers/common/abstract_hierarchy_viewer_presenter';
40import {VISIBLE_CHIP} from 'viewers/common/chip';
41import {
42  SfCuratedProperties,
43  SfLayerSummary,
44  SfSummaryProperty,
45} from 'viewers/common/curated_properties';
46import {DisplayIdentifier} from 'viewers/common/display_identifier';
47import {HierarchyPresenter} from 'viewers/common/hierarchy_presenter';
48import {PropertiesPresenter} from 'viewers/common/properties_presenter';
49import {RectsPresenter} from 'viewers/common/rects_presenter';
50import {TextFilter} from 'viewers/common/text_filter';
51import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
52import {UI_RECT_FACTORY} from 'viewers/common/ui_rect_factory';
53import {UserOptions} from 'viewers/common/user_options';
54import {UiRect} from 'viewers/components/rects/ui_rect';
55import {UiData} from './ui_data';
56
57export class Presenter extends AbstractHierarchyViewerPresenter<UiData> {
58  static readonly DENYLIST_PROPERTY_NAMES = [
59    'name',
60    'children',
61    'dpiX',
62    'dpiY',
63  ];
64
65  protected override hierarchyPresenter = new HierarchyPresenter(
66    PersistentStoreProxy.new<UserOptions>(
67      'SfHierarchyOptions',
68      {
69        showDiff: {
70          name: 'Show diff', // TODO: PersistentStoreObject.Ignored("Show diff") or something like that to instruct to not store this info
71          enabled: false,
72          isUnavailable: false,
73        },
74        showOnlyVisible: {
75          name: 'Show only',
76          chip: VISIBLE_CHIP,
77          enabled: false,
78        },
79        simplifyNames: {
80          name: 'Simplify names',
81          enabled: true,
82        },
83        flat: {
84          name: 'Flat',
85          enabled: false,
86        },
87      },
88      this.storage,
89    ),
90    new TextFilter(),
91    Presenter.DENYLIST_PROPERTY_NAMES,
92    true,
93    false,
94    this.getEntryFormattedTimestamp,
95  );
96  protected override rectsPresenter = new RectsPresenter(
97    PersistentStoreProxy.new<UserOptions>(
98      'SfRectsOptions',
99      {
100        ignoreRectShowState: {
101          name: 'Ignore',
102          icon: 'visibility',
103          enabled: false,
104        },
105        showOnlyVisible: {
106          name: 'Show only',
107          chip: VISIBLE_CHIP,
108          enabled: false,
109        },
110      },
111      this.storage,
112    ),
113    (tree: HierarchyTreeNode) =>
114      UI_RECT_FACTORY.makeUiRects(tree, this.viewCapturePackageNames),
115    (displays: UiRect[]) =>
116      makeDisplayIdentifiers(displays, this.wmFocusedDisplayId),
117    convertRectIdToLayerorDisplayName,
118  );
119  protected override propertiesPresenter = new PropertiesPresenter(
120    PersistentStoreProxy.new<UserOptions>(
121      'SfPropertyOptions',
122      {
123        showDiff: {
124          name: 'Show diff',
125          enabled: false,
126          isUnavailable: false,
127        },
128        showDefaults: {
129          name: 'Show defaults',
130          enabled: false,
131          tooltip: `If checked, shows the value of all properties.
132Otherwise, hides all properties whose value is
133the default for its data type.`,
134        },
135      },
136      this.storage,
137    ),
138    new TextFilter(),
139    Presenter.DENYLIST_PROPERTY_NAMES,
140    undefined,
141    ['a', 'type'],
142  );
143  protected override multiTraceType = undefined;
144
145  private viewCapturePackageNames: string[] | undefined;
146  private curatedProperties: SfCuratedProperties | undefined;
147  private wmTrace: Trace<HierarchyTreeNode> | undefined;
148  private wmFocusedDisplayId: number | undefined;
149
150  constructor(
151    trace: Trace<HierarchyTreeNode>,
152    traces: Traces,
153    storage: Readonly<Store>,
154    notifyViewCallback: NotifyHierarchyViewCallbackType<UiData>,
155  ) {
156    super(trace, traces, storage, notifyViewCallback, new UiData());
157    this.wmTrace = traces.getTrace(TraceType.WINDOW_MANAGER);
158  }
159
160  async onRectDoubleClick(rectId: string) {
161    if (!this.viewCapturePackageNames) {
162      return;
163    }
164    const rectHasViewCapture = this.viewCapturePackageNames.some(
165      (packageName) => rectId.includes(packageName),
166    );
167    if (!rectHasViewCapture) {
168      return;
169    }
170    const newActiveTrace = assertDefined(
171      this.traces.getTrace(TraceType.VIEW_CAPTURE),
172    );
173    await this.emitWinscopeEvent(new TabbedViewSwitchRequest(newActiveTrace));
174  }
175
176  override async onHighlightedNodeChange(item: UiHierarchyTreeNode) {
177    await this.applyHighlightedNodeChange(item);
178    this.updateCuratedProperties();
179    this.refreshUIData();
180  }
181
182  override async onHighlightedIdChange(newId: string) {
183    await this.applyHighlightedIdChange(newId);
184    this.updateCuratedProperties();
185    this.refreshUIData();
186  }
187
188  protected override getOverrideDisplayName(
189    selected: [Trace<HierarchyTreeNode>, HierarchyTreeNode],
190  ): string | undefined {
191    return selected[1].isRoot()
192      ? this.hierarchyPresenter.getCurrentHierarchyTreeNames(selected[0])?.at(0)
193      : undefined;
194  }
195
196  protected override keepCalculated(tree: HierarchyTreeNode): boolean {
197    return tree.isRoot();
198  }
199
200  protected override async initializeIfNeeded(event: TracePositionUpdate) {
201    if (!this.viewCapturePackageNames) {
202      const tracesVc = this.traces.getTraces(TraceType.VIEW_CAPTURE);
203      const promisesPackageName = tracesVc.map(async (trace) => {
204        const packageAndWindow = await trace.customQuery(
205          CustomQueryType.VIEW_CAPTURE_METADATA,
206        );
207        return packageAndWindow.packageName;
208      });
209      this.viewCapturePackageNames = await Promise.all(promisesPackageName);
210    }
211    await this.setInitialWmActiveDisplay(event);
212  }
213
214  protected override async processDataAfterPositionUpdate(): Promise<void> {
215    this.updateCuratedProperties();
216  }
217
218  protected override refreshUIData() {
219    this.uiData.curatedProperties = this.curatedProperties;
220    this.refreshHierarchyViewerUiData();
221  }
222
223  private updateCuratedProperties() {
224    const selectedHierarchyTree = this.hierarchyPresenter.getSelectedTree();
225    const propertiesTree = this.propertiesPresenter.getPropertiesTree();
226
227    if (selectedHierarchyTree && propertiesTree) {
228      if (selectedHierarchyTree[1].isRoot()) {
229        this.curatedProperties = undefined;
230      } else {
231        this.curatedProperties = this.getCuratedProperties(
232          selectedHierarchyTree[1],
233          propertiesTree,
234        );
235      }
236    } else {
237      this.curatedProperties = undefined;
238    }
239  }
240
241  private getCuratedProperties(
242    hTree: HierarchyTreeNode,
243    pTree: PropertyTreeNode,
244  ): SfCuratedProperties {
245    const inputWindowInfo = pTree.getChildByName('inputWindowInfo');
246    const hasInputChannel =
247      inputWindowInfo !== undefined &&
248      inputWindowInfo.getAllChildren().length > 0;
249
250    const cropLayerId = hasInputChannel
251      ? assertDefined(
252          inputWindowInfo.getChildByName('cropLayerId'),
253        ).formattedValue()
254      : '-1';
255
256    const verboseFlags = pTree.getChildByName('verboseFlags')?.formattedValue();
257    const flags = assertDefined(pTree.getChildByName('flags'));
258    const curatedFlags =
259      verboseFlags !== '' && verboseFlags !== undefined
260        ? verboseFlags
261        : flags.formattedValue();
262
263    const bufferTransform = pTree.getChildByName('bufferTransform');
264    const bufferTransformTypeFlags =
265      bufferTransform?.getChildByName('type')?.formattedValue() ?? 'null';
266
267    const zOrderRelativeOfNode = assertDefined(
268      pTree.getChildByName('zOrderRelativeOf'),
269    );
270    let relativeParent: string | SfLayerSummary =
271      zOrderRelativeOfNode.formattedValue();
272    if (relativeParent !== 'none') {
273      // update zOrderRelativeOf property formatter to zParent node id
274      zOrderRelativeOfNode.setFormatter(
275        new FixedStringFormatter(assertDefined(hTree.getZParent()).id),
276      );
277      relativeParent = this.getLayerSummary(
278        zOrderRelativeOfNode.formattedValue(),
279      );
280    }
281
282    const curated: SfCuratedProperties = {
283      summary: this.getSummaryOfVisibility(pTree),
284      flags: curatedFlags,
285      calcTransform: pTree.getChildByName('transform'),
286      calcCrop: assertDefined(pTree.getChildByName('bounds')).formattedValue(),
287      finalBounds: assertDefined(
288        pTree.getChildByName('screenBounds'),
289      ).formattedValue(),
290      reqTransform: pTree.getChildByName('requestedTransform'),
291      reqCrop: this.getCropPropertyValue(pTree, 'bounds'),
292      bufferSize: assertDefined(
293        pTree.getChildByName('activeBuffer'),
294      ).formattedValue(),
295      frameNumber: assertDefined(
296        pTree.getChildByName('currFrame'),
297      ).formattedValue(),
298      bufferTransformType: bufferTransformTypeFlags,
299      destinationFrame: assertDefined(
300        pTree.getChildByName('destinationFrame'),
301      ).formattedValue(),
302      z: assertDefined(pTree.getChildByName('z')).formattedValue(),
303      relativeParent,
304      relativeChildren: hTree
305        .getRelativeChildren()
306        .map((c) => this.getLayerSummary(c.id)),
307      calcColor: this.getColorPropertyValue(pTree, 'color'),
308      calcShadowRadius: this.getPixelPropertyValue(pTree, 'shadowRadius'),
309      calcCornerRadius: this.getPixelPropertyValue(pTree, 'cornerRadius'),
310      calcCornerRadiusCrop: this.getCropPropertyValue(
311        pTree,
312        'cornerRadiusCrop',
313      ),
314      backgroundBlurRadius: this.getPixelPropertyValue(
315        pTree,
316        'backgroundBlurRadius',
317      ),
318      reqColor: this.getColorPropertyValue(pTree, 'requestedColor'),
319      reqCornerRadius: this.getPixelPropertyValue(
320        pTree,
321        'requestedCornerRadius',
322      ),
323      inputTransform: inputWindowInfo?.getChildByName('transform'),
324      inputRegion: inputWindowInfo
325        ?.getChildByName('touchableRegion')
326        ?.formattedValue(),
327      focusable: hasInputChannel
328        ? assertDefined(
329            inputWindowInfo.getChildByName('focusable'),
330          ).formattedValue()
331        : 'null',
332      cropTouchRegionWithItem: cropLayerId,
333      replaceTouchRegionWithCrop: hasInputChannel
334        ? inputWindowInfo
335            .getChildByName('replaceTouchableRegionWithCrop')
336            ?.formattedValue() ?? 'false'
337        : 'false',
338      inputConfig:
339        inputWindowInfo?.getChildByName('inputConfig')?.formattedValue() ??
340        'null',
341      ignoreDestinationFrame:
342        (flags.getValue() & LayerFlag.IGNORE_DESTINATION_FRAME) ===
343        LayerFlag.IGNORE_DESTINATION_FRAME,
344      hasInputChannel,
345    };
346    return curated;
347  }
348
349  private getSummaryOfVisibility(tree: PropertyTreeNode): SfSummaryProperty[] {
350    const summary: SfSummaryProperty[] = [];
351    const visibilityReason = tree.getChildByName('visibilityReason');
352    if (visibilityReason && visibilityReason.getAllChildren().length > 0) {
353      const reason = this.mapNodeArrayToString(
354        visibilityReason.getAllChildren(),
355      );
356      summary.push({key: 'Invisible due to', simpleValue: reason});
357    }
358
359    const occludedBy = tree.getChildByName('occludedBy')?.getAllChildren();
360    if (occludedBy && occludedBy.length > 0) {
361      summary.push({
362        key: 'Occluded by',
363        layerValues: occludedBy.map((layer) =>
364          this.getLayerSummary(layer.formattedValue()),
365        ),
366        desc: 'Fully occluded by these opaque layers',
367      });
368    }
369
370    const partiallyOccludedBy = tree
371      .getChildByName('partiallyOccludedBy')
372      ?.getAllChildren();
373    if (partiallyOccludedBy && partiallyOccludedBy.length > 0) {
374      summary.push({
375        key: 'Partially occluded by',
376        layerValues: partiallyOccludedBy.map((layer) =>
377          this.getLayerSummary(layer.formattedValue()),
378        ),
379        desc: 'Partially occluded by these opaque layers',
380      });
381    }
382
383    const coveredBy = tree.getChildByName('coveredBy')?.getAllChildren();
384    if (coveredBy && coveredBy.length > 0) {
385      summary.push({
386        key: 'Covered by',
387        layerValues: coveredBy.map((layer) =>
388          this.getLayerSummary(layer.formattedValue()),
389        ),
390        desc: 'Partially or fully covered by these likely translucent layers',
391      });
392    }
393    return summary;
394  }
395
396  private mapNodeArrayToString(nodes: readonly PropertyTreeNode[]): string {
397    return nodes.map((reason) => reason.formattedValue()).join(', ');
398  }
399
400  private getLayerSummary(nodeId: string): SfLayerSummary {
401    const parts = nodeId.split(' ');
402    return {
403      layerId: parts[0],
404      nodeId,
405      name: parts.slice(1).join(' '),
406    };
407  }
408
409  private getPixelPropertyValue(tree: PropertyTreeNode, label: string): string {
410    const propVal = assertDefined(tree.getChildByName(label)).formattedValue();
411    return propVal !== 'null' ? `${propVal} px` : '0 px';
412  }
413
414  private getCropPropertyValue(tree: PropertyTreeNode, label: string): string {
415    const propVal = assertDefined(tree.getChildByName(label)).formattedValue();
416    return propVal !== 'null' ? propVal : EMPTY_OBJ_STRING;
417  }
418
419  private getColorPropertyValue(tree: PropertyTreeNode, label: string): string {
420    const propVal = assertDefined(tree.getChildByName(label)).formattedValue();
421    return propVal !== 'null' ? propVal : 'no color found';
422  }
423
424  private async setInitialWmActiveDisplay(event: TracePositionUpdate) {
425    if (!this.wmTrace || this.wmFocusedDisplayId !== undefined) {
426      return;
427    }
428    const wmEntry: HierarchyTreeNode | undefined =
429      await TraceEntryFinder.findCorrespondingEntry<HierarchyTreeNode>(
430        this.wmTrace,
431        event.position,
432      )?.getValue();
433    if (wmEntry) {
434      this.wmFocusedDisplayId = wmEntry
435        .getEagerPropertyByName('focusedDisplayId')
436        ?.getValue();
437    }
438  }
439}
440
441export function makeDisplayIdentifiers(
442  rects: UiRect[],
443  focusedDisplayId?: number,
444): DisplayIdentifier[] {
445  const ids: DisplayIdentifier[] = [];
446
447  const isActive = (display: UiRect) => {
448    if (focusedDisplayId !== undefined) {
449      return display.groupId === focusedDisplayId;
450    }
451    return display.isActiveDisplay;
452  };
453
454  rects.forEach((rect: UiRect) => {
455    if (!rect.isDisplay) return;
456
457    const displayId = rect.id.slice(10, rect.id.length);
458    ids.push({
459      displayId,
460      groupId: rect.groupId,
461      name: rect.label,
462      isActive: isActive(rect),
463    });
464  });
465
466  let offscreenDisplayCount = 0;
467  rects.forEach((rect: UiRect) => {
468    if (rect.isDisplay) return;
469
470    if (!ids.find((identifier) => identifier.groupId === rect.groupId)) {
471      offscreenDisplayCount++;
472      const name =
473        'Offscreen Display' +
474        (offscreenDisplayCount > 1 ? ` ${offscreenDisplayCount}` : '');
475      ids.push({displayId: -1, groupId: rect.groupId, name, isActive: false});
476    }
477  });
478
479  return ids;
480}
481
482export function convertRectIdToLayerorDisplayName(id: string) {
483  if (id.startsWith('Display')) return id.split('-').slice(1).join('-').trim();
484  const idMinusStartLayerId = id.split(' ').slice(1).join(' ');
485  const idSplittingEndLayerId = idMinusStartLayerId.split('#');
486  return idSplittingEndLayerId
487    .slice(0, idSplittingEndLayerId.length - 1)
488    .join('#');
489}
490