1/*
2 * Copyright (C) 2023 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, assertTrue} from 'common/assert_utils';
18import {PersistentStoreProxy} from 'common/persistent_store_proxy';
19import {
20  TabbedViewSwitchRequest,
21  WinscopeEvent,
22  WinscopeEventType,
23} from 'messaging/winscope_event';
24import {CustomQueryType} from 'trace/custom_query';
25import {Trace} from 'trace/trace';
26import {Traces} from 'trace/traces';
27import {TraceEntryFinder} from 'trace/trace_entry_finder';
28import {TraceType} from 'trace/trace_type';
29import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
30import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
31import {
32  AbstractHierarchyViewerPresenter,
33  NotifyHierarchyViewCallbackType,
34} from 'viewers/common/abstract_hierarchy_viewer_presenter';
35import {VISIBLE_CHIP} from 'viewers/common/chip';
36import {VcCuratedProperties} from 'viewers/common/curated_properties';
37import {DisplayIdentifier} from 'viewers/common/display_identifier';
38import {HierarchyPresenter} from 'viewers/common/hierarchy_presenter';
39import {PropertiesPresenter} from 'viewers/common/properties_presenter';
40import {RectsPresenter} from 'viewers/common/rects_presenter';
41import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
42import {UI_RECT_FACTORY} from 'viewers/common/ui_rect_factory';
43import {UserOptions} from 'viewers/common/user_options';
44import {UiRect} from 'viewers/components/rects/types2d';
45import {UiData} from './ui_data';
46
47export class Presenter extends AbstractHierarchyViewerPresenter {
48  static readonly DENYLIST_PROPERTY_NAMES = ['children', 'isComputedVisible'];
49
50  private windowNames: string[] = [];
51  protected override hierarchyPresenter = new HierarchyPresenter(
52    PersistentStoreProxy.new<UserOptions>(
53      'VcHierarchyOptions',
54      {
55        showDiff: {
56          name: 'Show diff',
57          enabled: false,
58          isUnavailable: false,
59        },
60        showOnlyVisible: {
61          name: 'Show only',
62          chip: VISIBLE_CHIP,
63          enabled: false,
64        },
65        simplifyNames: {
66          name: 'Simplify names',
67          enabled: true,
68        },
69      },
70      this.storage,
71    ),
72    Presenter.DENYLIST_PROPERTY_NAMES,
73    false,
74    true,
75  );
76  protected override rectsPresenter = new RectsPresenter(
77    PersistentStoreProxy.new<UserOptions>(
78      'VcRectsOptions',
79      {
80        ignoreNonHidden: {
81          name: 'Ignore',
82          icon: 'visibility',
83          enabled: false,
84        },
85        showOnlyVisible: {
86          name: 'Show only',
87          chip: VISIBLE_CHIP,
88          enabled: false,
89        },
90      },
91      this.storage,
92    ),
93    (tree: HierarchyTreeNode, trace: Trace<HierarchyTreeNode>) =>
94      UI_RECT_FACTORY.makeVcUiRects(
95        tree,
96        this.getIdFromViewCaptureTrace(trace),
97      ),
98  );
99  protected override propertiesPresenter = new PropertiesPresenter(
100    PersistentStoreProxy.new<UserOptions>(
101      'VcPropertyOptions',
102      {
103        showDiff: {
104          name: 'Show diff',
105          enabled: false,
106          isUnavailable: false,
107        },
108        showDefaults: {
109          name: 'Show defaults',
110          enabled: false,
111          tooltip: `
112              If checked, shows the value of all properties.
113              Otherwise, hides all properties whose value is
114              the default for its data type.
115            `,
116        },
117      },
118      this.storage,
119    ),
120    Presenter.DENYLIST_PROPERTY_NAMES,
121  );
122  protected override readonly multiTraceType = TraceType.VIEW_CAPTURE;
123
124  private readonly surfaceFlingerTrace: Trace<HierarchyTreeNode> | undefined;
125  private readonly viewCaptureTraces: Array<Trace<HierarchyTreeNode>>;
126
127  private viewCapturePackageNames: string[] = [];
128  private sfRects: UiRect[] | undefined;
129  private curatedProperties: VcCuratedProperties | undefined;
130
131  constructor(
132    traces: Traces,
133    storage: Readonly<Storage>,
134    notifyViewCallback: NotifyHierarchyViewCallbackType,
135  ) {
136    super(undefined, traces, storage, notifyViewCallback, new UiData());
137    this.viewCaptureTraces = traces.getTraces(TraceType.VIEW_CAPTURE);
138    this.surfaceFlingerTrace = traces.getTrace(TraceType.SURFACE_FLINGER);
139  }
140
141  async onMiniRectsDoubleClick() {
142    if (!this.surfaceFlingerTrace) {
143      return;
144    }
145    await this.emitWinscopeEvent(
146      new TabbedViewSwitchRequest(this.surfaceFlingerTrace),
147    );
148  }
149
150  getTraces(): Array<Trace<HierarchyTreeNode>> {
151    return this.viewCaptureTraces;
152  }
153
154  getViewCaptureTraceFromId(id: number): Trace<HierarchyTreeNode> {
155    return assertDefined(this.viewCaptureTraces[id]);
156  }
157
158  override async onAppEvent(event: WinscopeEvent) {
159    await event.visit(
160      WinscopeEventType.TRACE_POSITION_UPDATE,
161      async (event) => {
162        await this.initializeIfNeeded();
163        await this.applyTracePositionUpdate(event);
164
165        if (this.uiData && this.surfaceFlingerTrace) {
166          const surfaceFlingerEntry =
167            (await TraceEntryFinder.findCorrespondingEntry(
168              this.surfaceFlingerTrace,
169              event.position,
170            )?.getValue()) as HierarchyTreeNode;
171          if (surfaceFlingerEntry) {
172            this.sfRects = UI_RECT_FACTORY.makeUiRects(
173              surfaceFlingerEntry,
174              this.viewCapturePackageNames,
175            );
176          }
177        }
178        this.updateCuratedProperties();
179        this.refreshUIData();
180      },
181    );
182  }
183
184  override async onHighlightedNodeChange(node: UiHierarchyTreeNode) {
185    await this.applyHighlightedNodeChange(node);
186    this.updateCuratedProperties();
187    this.refreshUIData();
188  }
189
190  override async onHighlightedIdChange(newId: string) {
191    await this.applyHighlightedIdChange(newId);
192    this.updateCuratedProperties();
193    this.refreshUIData();
194  }
195
196  protected override getOverrideDisplayName(): undefined {
197    return undefined;
198  }
199
200  protected override keepCalculated(): boolean {
201    return true;
202  }
203
204  private async initializeIfNeeded() {
205    await this.initializePackageNamesIfNeeded();
206    await this.initializeWindowsIfNeeded();
207  }
208
209  private async initializePackageNamesIfNeeded() {
210    if (this.viewCapturePackageNames.length > 0) {
211      return;
212    }
213
214    const promisesPackageName = this.viewCaptureTraces.map(async (trace) => {
215      const packageAndWindow = await trace.customQuery(
216        CustomQueryType.VIEW_CAPTURE_METADATA,
217      );
218      return packageAndWindow.packageName;
219    });
220
221    this.viewCapturePackageNames = await Promise.all(promisesPackageName);
222  }
223
224  private async initializeWindowsIfNeeded() {
225    if (this.rectsPresenter.getDisplays().length > 0) {
226      return;
227    }
228
229    const shortenAndCapitalizeWindowName = (name: string) => {
230      const lastDot = name.lastIndexOf('.');
231      if (lastDot !== -1) {
232        name = name.substring(lastDot + 1);
233      }
234      if (name.length > 0) {
235        name = name[0].toUpperCase() + name.slice(1);
236      }
237      return name;
238    };
239
240    const promisesWindowName = this.viewCaptureTraces.map(async (trace) => {
241      const packageAndWindow = await trace.customQuery(
242        CustomQueryType.VIEW_CAPTURE_METADATA,
243      );
244      return shortenAndCapitalizeWindowName(packageAndWindow.windowName);
245    });
246    this.windowNames = await Promise.all(promisesWindowName);
247    this.rectsPresenter.setDisplays(this.getWindows(this.windowNames));
248  }
249
250  private getWindows(windowNames: string[]): DisplayIdentifier[] {
251    return this.viewCaptureTraces
252      .map((trace, i) => {
253        const traceId = this.getIdFromViewCaptureTrace(trace);
254        return {
255          displayId: traceId,
256          groupId: traceId,
257          name: windowNames[i],
258        };
259      })
260      .sort((a, b) => a.name.localeCompare(b.name));
261  }
262
263  private updateCuratedProperties() {
264    const propertiesTree = this.propertiesPresenter.getPropertiesTree();
265    if (propertiesTree) {
266      this.curatedProperties = this.getCuratedProperties(propertiesTree);
267    } else {
268      this.curatedProperties = undefined;
269    }
270  }
271
272  private getCuratedProperties(tree: PropertyTreeNode): VcCuratedProperties {
273    const curated: VcCuratedProperties = {
274      className: tree.name,
275      hashcode: assertDefined(tree.getChildByName('hashcode')).formattedValue(),
276      left: assertDefined(tree.getChildByName('left')).formattedValue(),
277      top: assertDefined(tree.getChildByName('top')).formattedValue(),
278      elevation: assertDefined(
279        tree.getChildByName('elevation'),
280      ).formattedValue(),
281      height: assertDefined(tree.getChildByName('height')).formattedValue(),
282      width: assertDefined(tree.getChildByName('width')).formattedValue(),
283      translationX: assertDefined(
284        tree.getChildByName('translationX'),
285      ).formattedValue(),
286      translationY: assertDefined(
287        tree.getChildByName('translationY'),
288      ).formattedValue(),
289      scrollX: assertDefined(tree.getChildByName('scrollX')).formattedValue(),
290      scrollY: assertDefined(tree.getChildByName('scrollY')).formattedValue(),
291      scaleX: assertDefined(tree.getChildByName('scaleX')).formattedValue(),
292      scaleY: assertDefined(tree.getChildByName('scaleY')).formattedValue(),
293      visibility: assertDefined(
294        tree.getChildByName('visibility'),
295      ).formattedValue(),
296      alpha: assertDefined(tree.getChildByName('alpha')).formattedValue(),
297      willNotDraw: assertDefined(
298        tree.getChildByName('willNotDraw'),
299      ).formattedValue(),
300      clipChildren: assertDefined(
301        tree.getChildByName('clipChildren'),
302      ).formattedValue(),
303    };
304    return curated;
305  }
306
307  private getIdFromViewCaptureTrace(trace: Trace<HierarchyTreeNode>): number {
308    const index = this.viewCaptureTraces.indexOf(trace);
309    assertTrue(index !== -1);
310    return index;
311  }
312
313  private refreshUIData() {
314    this.refreshHierarchyViewerUiData(
315      new UiData(this.sfRects, this.curatedProperties),
316    );
317  }
318}
319