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 {FunctionUtils} from 'common/function_utils';
19import {TracePositionUpdate, WinscopeEvent} from 'messaging/winscope_event';
20import {
21  EmitEvent,
22  WinscopeEventEmitter,
23} from 'messaging/winscope_event_emitter';
24import {Trace, TraceEntry} from 'trace/trace';
25import {Traces} from 'trace/traces';
26import {TraceEntryFinder} from 'trace/trace_entry_finder';
27import {TraceType} from 'trace/trace_type';
28import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
29import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
30import {PropertiesPresenter} from 'viewers/common/properties_presenter';
31import {RectsPresenter} from 'viewers/common/rects_presenter';
32import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
33import {UserOptions} from 'viewers/common/user_options';
34import {HierarchyPresenter} from './hierarchy_presenter';
35import {RectShowState} from './rect_show_state';
36import {UiDataHierarchy} from './ui_data_hierarchy';
37import {ViewerEvents} from './viewer_events';
38
39export type NotifyHierarchyViewCallbackType = (uiData: UiDataHierarchy) => void;
40
41export abstract class AbstractHierarchyViewerPresenter
42  implements WinscopeEventEmitter
43{
44  protected emitWinscopeEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
45  protected overridePropertiesTree: PropertyTreeNode | undefined;
46  protected overridePropertiesTreeName: string | undefined;
47  protected rectsPresenter?: RectsPresenter;
48  protected abstract hierarchyPresenter: HierarchyPresenter;
49  protected abstract propertiesPresenter: PropertiesPresenter;
50  protected abstract readonly multiTraceType?: TraceType;
51  private highlightedItem = '';
52
53  constructor(
54    private readonly trace: Trace<HierarchyTreeNode> | undefined,
55    protected readonly traces: Traces,
56    protected readonly storage: Readonly<Storage>,
57    private readonly notifyViewCallback: NotifyHierarchyViewCallbackType,
58    protected uiData: UiDataHierarchy,
59  ) {
60    this.copyUiDataAndNotifyView();
61  }
62
63  setEmitEvent(callback: EmitEvent) {
64    this.emitWinscopeEvent = callback;
65  }
66
67  addEventListeners(htmlElement: HTMLElement) {
68    htmlElement.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) =>
69      this.onPinnedItemChange((event as CustomEvent).detail.pinnedItem),
70    );
71    htmlElement.addEventListener(
72      ViewerEvents.HighlightedIdChange,
73      async (event) =>
74        await this.onHighlightedIdChange((event as CustomEvent).detail.id),
75    );
76    htmlElement.addEventListener(
77      ViewerEvents.HighlightedPropertyChange,
78      (event) =>
79        this.onHighlightedPropertyChange((event as CustomEvent).detail.id),
80    );
81    htmlElement.addEventListener(
82      ViewerEvents.HierarchyUserOptionsChange,
83      async (event) =>
84        await this.onHierarchyUserOptionsChange(
85          (event as CustomEvent).detail.userOptions,
86        ),
87    );
88    htmlElement.addEventListener(
89      ViewerEvents.HierarchyFilterChange,
90      async (event) =>
91        await this.onHierarchyFilterChange(
92          (event as CustomEvent).detail.filterString,
93        ),
94    );
95    htmlElement.addEventListener(
96      ViewerEvents.PropertiesUserOptionsChange,
97      async (event) =>
98        await this.onPropertiesUserOptionsChange(
99          (event as CustomEvent).detail.userOptions,
100        ),
101    );
102    htmlElement.addEventListener(
103      ViewerEvents.PropertiesFilterChange,
104      async (event) =>
105        await this.onPropertiesFilterChange(
106          (event as CustomEvent).detail.filterString,
107        ),
108    );
109    htmlElement.addEventListener(
110      ViewerEvents.HighlightedNodeChange,
111      async (event) =>
112        await this.onHighlightedNodeChange((event as CustomEvent).detail.node),
113    );
114    htmlElement.addEventListener(
115      ViewerEvents.RectShowStateChange,
116      async (event) => {
117        await this.onRectShowStateChange(
118          (event as CustomEvent).detail.rectId,
119          (event as CustomEvent).detail.state,
120        );
121      },
122    );
123    htmlElement.addEventListener(
124      ViewerEvents.RectsUserOptionsChange,
125      (event) => {
126        this.onRectsUserOptionsChange(
127          (event as CustomEvent).detail.userOptions,
128        );
129      },
130    );
131  }
132
133  onPinnedItemChange(pinnedItem: UiHierarchyTreeNode) {
134    this.hierarchyPresenter.applyPinnedItemChange(pinnedItem);
135    this.uiData.pinnedItems = this.hierarchyPresenter.getPinnedItems();
136    this.copyUiDataAndNotifyView();
137  }
138
139  onHighlightedPropertyChange(id: string) {
140    this.propertiesPresenter.applyHighlightedPropertyChange(id);
141    this.uiData.highlightedProperty =
142      this.propertiesPresenter.getHighlightedProperty();
143    this.copyUiDataAndNotifyView();
144  }
145
146  onRectsUserOptionsChange(userOptions: UserOptions) {
147    if (!this.rectsPresenter) {
148      return;
149    }
150    this.rectsPresenter.applyRectsUserOptionsChange(userOptions);
151
152    this.uiData.rectsUserOptions = this.rectsPresenter.getUserOptions();
153    this.uiData.rectsToDraw = this.rectsPresenter.getRectsToDraw();
154    this.uiData.rectIdToShowState = this.rectsPresenter.getRectIdToShowState();
155
156    this.copyUiDataAndNotifyView();
157  }
158
159  async onHierarchyUserOptionsChange(userOptions: UserOptions) {
160    await this.hierarchyPresenter.applyHierarchyUserOptionsChange(userOptions);
161    this.uiData.hierarchyUserOptions = this.hierarchyPresenter.getUserOptions();
162    this.uiData.hierarchyTrees = this.hierarchyPresenter.getAllFormattedTrees();
163    this.copyUiDataAndNotifyView();
164  }
165
166  async onHierarchyFilterChange(filterString: string) {
167    await this.hierarchyPresenter.applyHierarchyFilterChange(filterString);
168    this.uiData.hierarchyTrees = this.hierarchyPresenter.getAllFormattedTrees();
169    this.copyUiDataAndNotifyView();
170  }
171
172  async onPropertiesUserOptionsChange(userOptions: UserOptions) {
173    if (!this.propertiesPresenter) {
174      return;
175    }
176    this.propertiesPresenter.applyPropertiesUserOptionsChange(userOptions);
177    await this.updatePropertiesTree();
178    this.uiData.propertiesUserOptions =
179      this.propertiesPresenter.getUserOptions();
180    this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree();
181    this.copyUiDataAndNotifyView();
182  }
183
184  async onPropertiesFilterChange(filterString: string) {
185    if (!this.propertiesPresenter) {
186      return;
187    }
188    this.propertiesPresenter.applyPropertiesFilterChange(filterString);
189    await this.updatePropertiesTree();
190    this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree();
191    this.copyUiDataAndNotifyView();
192  }
193
194  async onRectShowStateChange(id: string, newShowState: RectShowState) {
195    if (!this.rectsPresenter) {
196      return;
197    }
198    this.rectsPresenter.applyRectShowStateChange(id, newShowState);
199
200    this.uiData.rectsToDraw = this.rectsPresenter.getRectsToDraw();
201    this.uiData.rectIdToShowState = this.rectsPresenter.getRectIdToShowState();
202    this.copyUiDataAndNotifyView();
203  }
204
205  protected async applyTracePositionUpdate(event: TracePositionUpdate) {
206    let entries: Array<TraceEntry<HierarchyTreeNode>> = [];
207    if (this.multiTraceType !== undefined) {
208      entries = this.traces
209        .getTraces(this.multiTraceType)
210        .map((trace) => {
211          return TraceEntryFinder.findCorrespondingEntry(
212            trace,
213            event.position,
214          ) as TraceEntry<HierarchyTreeNode> | undefined;
215        })
216        .filter((entry) => entry !== undefined) as Array<
217        TraceEntry<HierarchyTreeNode>
218      >;
219    } else {
220      const entry = TraceEntryFinder.findCorrespondingEntry(
221        assertDefined(this.trace),
222        event.position,
223      );
224      if (entry) entries.push(entry);
225    }
226
227    await this.hierarchyPresenter.applyTracePositionUpdate(
228      entries,
229      this.highlightedItem,
230    );
231
232    const propertiesOpts = this.propertiesPresenter.getUserOptions();
233    const hasPreviousEntry = entries.some((e) => e.getIndex() > 0);
234    if (propertiesOpts['showDiff']?.isUnavailable !== undefined) {
235      propertiesOpts['showDiff'].isUnavailable = !hasPreviousEntry;
236    }
237
238    const currentHierarchyTrees =
239      this.hierarchyPresenter.getAllCurrentHierarchyTrees();
240    if (currentHierarchyTrees) {
241      this.rectsPresenter?.applyHierarchyTreesChange(currentHierarchyTrees);
242      await this.updatePropertiesTree();
243    }
244  }
245
246  protected async applyHighlightedNodeChange(node: UiHierarchyTreeNode) {
247    this.updateHighlightedItem(node.id);
248    this.hierarchyPresenter.applyHighlightedNodeChange(node);
249    await this.updatePropertiesTree();
250  }
251
252  protected async applyHighlightedIdChange(newId: string) {
253    this.updateHighlightedItem(newId);
254    this.hierarchyPresenter.applyHighlightedIdChange(newId);
255    await this.updatePropertiesTree();
256  }
257
258  protected async updatePropertiesTree() {
259    if (this.overridePropertiesTree) {
260      this.propertiesPresenter.setPropertiesTree(this.overridePropertiesTree);
261      await this.propertiesPresenter.formatPropertiesTree(
262        undefined,
263        this.overridePropertiesTreeName,
264        false,
265      );
266      return;
267    }
268    const selected = this.hierarchyPresenter.getSelectedTree();
269    if (selected) {
270      const [trace, selectedTree] = selected;
271      const propertiesTree = await selectedTree.getAllProperties();
272      if (
273        this.propertiesPresenter.getUserOptions()['showDiff']?.enabled &&
274        !this.hierarchyPresenter.getPreviousHierarchyTreeForTrace(trace)
275      ) {
276        await this.hierarchyPresenter.updatePreviousHierarchyTrees();
277      }
278      const previousTree =
279        this.hierarchyPresenter.getPreviousHierarchyTreeForTrace(trace);
280      this.propertiesPresenter.setPropertiesTree(propertiesTree);
281      await this.propertiesPresenter.formatPropertiesTree(
282        previousTree,
283        this.getOverrideDisplayName(selected),
284        this.keepCalculated(selectedTree),
285      );
286    } else {
287      this.propertiesPresenter.clear();
288    }
289  }
290
291  protected updateHighlightedItem(id: string) {
292    if (this.highlightedItem === id) {
293      this.highlightedItem = '';
294    } else {
295      this.highlightedItem = id;
296    }
297  }
298
299  protected refreshHierarchyViewerUiData(uiData: UiDataHierarchy) {
300    this.uiData = uiData;
301    this.uiData.highlightedItem = this.highlightedItem;
302    this.uiData.pinnedItems = this.hierarchyPresenter.getPinnedItems();
303    this.uiData.hierarchyUserOptions = this.hierarchyPresenter.getUserOptions();
304    this.uiData.hierarchyTrees = this.hierarchyPresenter.getAllFormattedTrees();
305
306    this.uiData.propertiesUserOptions =
307      this.propertiesPresenter.getUserOptions();
308    this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree();
309    this.uiData.highlightedProperty =
310      this.propertiesPresenter.getHighlightedProperty();
311
312    if (this.rectsPresenter) {
313      this.uiData.rectsToDraw = this.rectsPresenter?.getRectsToDraw();
314      this.uiData.rectIdToShowState =
315        this.rectsPresenter.getRectIdToShowState();
316      this.uiData.displays = this.rectsPresenter.getDisplays();
317      this.uiData.rectsUserOptions = this.rectsPresenter.getUserOptions();
318    }
319
320    this.copyUiDataAndNotifyView();
321  }
322
323  protected getHighlightedItem(): string | undefined {
324    return this.highlightedItem;
325  }
326
327  private copyUiDataAndNotifyView() {
328    // Create a shallow copy of the data, otherwise the Angular OnPush change detection strategy
329    // won't detect the new input
330    const copy = Object.assign({}, this.uiData);
331    this.notifyViewCallback(copy);
332  }
333
334  abstract onAppEvent(event: WinscopeEvent): Promise<void>;
335  abstract onHighlightedNodeChange(node: UiHierarchyTreeNode): Promise<void>;
336  abstract onHighlightedIdChange(id: string): Promise<void>;
337  protected abstract keepCalculated(tree: HierarchyTreeNode): boolean;
338  protected abstract getOverrideDisplayName(
339    selected: [Trace<HierarchyTreeNode>, HierarchyTreeNode],
340  ): string | undefined;
341}
342