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