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