• 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 {FilterType, TreeUtils} from 'common/tree_utils';
20import {Layer} from 'trace/flickerlib/layers/Layer';
21import {LayerTraceEntry} from 'trace/flickerlib/layers/LayerTraceEntry';
22import {Trace} from 'trace/trace';
23import {Traces} from 'trace/traces';
24import {TraceEntryFinder} from 'trace/trace_entry_finder';
25import {TracePosition} from 'trace/trace_position';
26import {TraceType} from 'trace/trace_type';
27import {Rectangle, RectMatrix, RectTransform} from 'viewers/common/rectangle';
28import {TreeGenerator} from 'viewers/common/tree_generator';
29import {TreeTransformer} from 'viewers/common/tree_transformer';
30import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
31import {UserOptions} from 'viewers/common/user_options';
32import {UiData} from './ui_data';
33
34type NotifyViewCallbackType = (uiData: UiData) => void;
35
36export class Presenter {
37  private readonly notifyViewCallback: NotifyViewCallbackType;
38  private readonly trace: Trace<LayerTraceEntry>;
39  private uiData: UiData;
40  private hierarchyFilter: FilterType = TreeUtils.makeNodeFilter('');
41  private propertiesFilter: FilterType = TreeUtils.makeNodeFilter('');
42  private highlightedItems: string[] = [];
43  private displayIds: number[] = [];
44  private pinnedItems: HierarchyTreeNode[] = [];
45  private pinnedIds: string[] = [];
46  private selectedHierarchyTree: HierarchyTreeNode | null = null;
47  private selectedLayer: LayerTraceEntry | Layer | null = null;
48  private previousEntry: LayerTraceEntry | null = null;
49  private entry: LayerTraceEntry | null = null;
50  private hierarchyUserOptions: UserOptions = PersistentStoreProxy.new<UserOptions>(
51    'SfHierarchyOptions',
52    {
53      showDiff: {
54        name: 'Show diff', // TODO: PersistentStoreObject.Ignored("Show diff") or something like that to instruct to not store this info
55        enabled: false,
56      },
57      simplifyNames: {
58        name: 'Simplify names',
59        enabled: true,
60      },
61      onlyVisible: {
62        name: 'Only visible',
63        enabled: false,
64      },
65      flat: {
66        name: 'Flat',
67        enabled: false,
68      },
69    },
70    this.storage
71  );
72
73  private propertiesUserOptions: UserOptions = PersistentStoreProxy.new<UserOptions>(
74    'SfPropertyOptions',
75    {
76      showDiff: {
77        name: 'Show diff',
78        enabled: false,
79      },
80      showDefaults: {
81        name: 'Show defaults',
82        enabled: false,
83        tooltip: `
84                If checked, shows the value of all properties.
85                Otherwise, hides all properties whose value is
86                the default for its data type.
87              `,
88      },
89    },
90    this.storage
91  );
92
93  constructor(
94    traces: Traces,
95    private readonly storage: Storage,
96    notifyViewCallback: NotifyViewCallbackType
97  ) {
98    this.trace = assertDefined(traces.getTrace(TraceType.SURFACE_FLINGER));
99    this.notifyViewCallback = notifyViewCallback;
100    this.uiData = new UiData([TraceType.SURFACE_FLINGER]);
101    this.copyUiDataAndNotifyView();
102  }
103
104  async onTracePositionUpdate(position: TracePosition) {
105    this.uiData = new UiData();
106    this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
107    this.uiData.propertiesUserOptions = this.propertiesUserOptions;
108
109    const entry = TraceEntryFinder.findCorrespondingEntry(this.trace, position);
110    const prevEntry =
111      entry && entry.getIndex() > 0 ? this.trace.getEntry(entry.getIndex() - 1) : undefined;
112
113    this.entry = (await entry?.getValue()) ?? null;
114    this.previousEntry = (await prevEntry?.getValue()) ?? null;
115    if (this.entry) {
116      this.uiData.highlightedItems = this.highlightedItems;
117      this.uiData.rects = this.generateRects();
118      this.uiData.displayIds = this.displayIds;
119      this.uiData.tree = this.generateTree();
120    }
121
122    this.copyUiDataAndNotifyView();
123  }
124
125  updatePinnedItems(pinnedItem: HierarchyTreeNode) {
126    const pinnedId = `${pinnedItem.id}`;
127    if (this.pinnedItems.map((item) => `${item.id}`).includes(pinnedId)) {
128      this.pinnedItems = this.pinnedItems.filter((pinned) => `${pinned.id}` !== pinnedId);
129    } else {
130      this.pinnedItems.push(pinnedItem);
131    }
132    this.updatePinnedIds(pinnedId);
133    this.uiData.pinnedItems = this.pinnedItems;
134    this.copyUiDataAndNotifyView();
135  }
136
137  updateHighlightedItems(id: string) {
138    if (this.highlightedItems.includes(id)) {
139      this.highlightedItems = this.highlightedItems.filter((hl) => hl !== id);
140    } else {
141      this.highlightedItems = []; //if multi-select surfaces implemented, remove this line
142      this.highlightedItems.push(id);
143    }
144    this.uiData.highlightedItems = this.highlightedItems;
145    this.copyUiDataAndNotifyView();
146  }
147
148  updateHierarchyTree(userOptions: UserOptions) {
149    this.hierarchyUserOptions = userOptions;
150    this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
151    this.uiData.tree = this.generateTree();
152    this.copyUiDataAndNotifyView();
153  }
154
155  filterHierarchyTree(filterString: string) {
156    this.hierarchyFilter = TreeUtils.makeNodeFilter(filterString);
157    this.uiData.tree = this.generateTree();
158    this.copyUiDataAndNotifyView();
159  }
160
161  updatePropertiesTree(userOptions: UserOptions) {
162    this.propertiesUserOptions = userOptions;
163    this.uiData.propertiesUserOptions = this.propertiesUserOptions;
164    this.updateSelectedTreeUiData();
165  }
166
167  filterPropertiesTree(filterString: string) {
168    this.propertiesFilter = TreeUtils.makeNodeFilter(filterString);
169    this.updateSelectedTreeUiData();
170  }
171
172  newPropertiesTree(selectedItem: HierarchyTreeNode) {
173    this.selectedHierarchyTree = selectedItem;
174    this.updateSelectedTreeUiData();
175  }
176
177  private generateRects(): Rectangle[] {
178    const displayRects =
179      this.entry.displays.map((display: any) => {
180        const rect = display.layerStackSpace;
181        rect.label = 'Display';
182        if (display.name) {
183          rect.label += ` - ${display.name}`;
184        }
185        rect.stableId = `Display - ${display.id}`;
186        rect.displayId = display.layerStackId;
187        rect.isDisplay = true;
188        rect.cornerRadius = 0;
189        rect.isVirtual = display.isVirtual ?? false;
190        rect.transform = {
191          matrix: display.transform.matrix,
192        };
193        return rect;
194      }) ?? [];
195    this.displayIds = this.entry.displays.map((it: any) => it.layerStackId);
196    this.displayIds.sort();
197    const rects = this.getLayersForRectsView()
198      .sort(this.compareLayerZ)
199      .map((it: any) => {
200        const rect = it.rect;
201        rect.displayId = it.stackId;
202        rect.cornerRadius = it.cornerRadius;
203        if (!this.displayIds.includes(it.stackId)) {
204          this.displayIds.push(it.stackId);
205        }
206        rect.transform = {
207          matrix: rect.transform.matrix,
208        };
209        return rect;
210      });
211
212    return this.rectsToUiData(rects.concat(displayRects));
213  }
214
215  private getLayersForRectsView(): Layer[] {
216    const onlyVisible = this.hierarchyUserOptions['onlyVisible']?.enabled ?? false;
217    // Show only visible layers or Visible + Occluded layers. Don't show all layers
218    // (flattenedLayers) because container layers are never meant to be displayed
219    return this.entry.flattenedLayers.filter(
220      (it: any) => it.isVisible || (!onlyVisible && it.occludedBy.length > 0)
221    );
222  }
223
224  private compareLayerZ(a: Layer, b: Layer): number {
225    const zipLength = Math.min(a.zOrderPath.length, b.zOrderPath.length);
226    for (let i = 0; i < zipLength; ++i) {
227      const zOrderA = a.zOrderPath[i];
228      const zOrderB = b.zOrderPath[i];
229      if (zOrderA > zOrderB) return -1;
230      if (zOrderA < zOrderB) return 1;
231    }
232    return b.zOrderPath.length - a.zOrderPath.length;
233  }
234
235  private updateSelectedTreeUiData() {
236    if (this.selectedHierarchyTree) {
237      this.uiData.propertiesTree = this.getTreeWithTransformedProperties(
238        this.selectedHierarchyTree
239      );
240      this.uiData.selectedLayer = this.selectedLayer;
241      this.uiData.displayPropertyGroups = this.shouldDisplayPropertyGroups(this.selectedLayer);
242    }
243    this.copyUiDataAndNotifyView();
244  }
245
246  private generateTree() {
247    if (!this.entry) {
248      return null;
249    }
250
251    const generator = new TreeGenerator(this.entry, this.hierarchyFilter, this.pinnedIds)
252      .setIsOnlyVisibleView(this.hierarchyUserOptions['onlyVisible']?.enabled)
253      .setIsSimplifyNames(this.hierarchyUserOptions['simplifyNames']?.enabled)
254      .setIsFlatView(this.hierarchyUserOptions['flat']?.enabled)
255      .withUniqueNodeId();
256    let tree: HierarchyTreeNode | null;
257    if (!this.hierarchyUserOptions['showDiff']?.enabled) {
258      tree = generator.generateTree();
259    } else {
260      tree = generator
261        .compareWith(this.previousEntry)
262        .withModifiedCheck()
263        .generateFinalTreeWithDiff();
264    }
265    this.pinnedItems = generator.getPinnedItems();
266    this.uiData.pinnedItems = this.pinnedItems;
267    return tree;
268  }
269
270  private rectsToUiData(rects: any[]): Rectangle[] {
271    const uiRects: Rectangle[] = [];
272    rects.forEach((rect: any) => {
273      let t = null;
274      if (rect.transform && rect.transform.matrix) {
275        t = rect.transform.matrix;
276      } else if (rect.transform) {
277        t = rect.transform;
278      }
279      let transform: RectTransform | null = null;
280      if (t !== null) {
281        const matrix: RectMatrix = {
282          dsdx: t.dsdx,
283          dsdy: t.dsdy,
284          dtdx: t.dtdx,
285          dtdy: t.dtdy,
286          tx: t.tx,
287          ty: t.ty,
288        };
289        transform = {
290          matrix,
291        };
292      }
293
294      const newRect: Rectangle = {
295        topLeft: {x: rect.left, y: rect.top},
296        bottomRight: {x: rect.right, y: rect.bottom},
297        label: rect.label,
298        transform,
299        isVisible: rect.ref?.isVisible ?? false,
300        isDisplay: rect.isDisplay ?? false,
301        ref: rect.ref,
302        id: rect.stableId ?? rect.ref.stableId,
303        displayId: rect.displayId ?? rect.ref.stackId,
304        isVirtual: rect.isVirtual ?? false,
305        isClickable: !(rect.isDisplay ?? false),
306        cornerRadius: rect.cornerRadius,
307      };
308      uiRects.push(newRect);
309    });
310    return uiRects;
311  }
312
313  private updatePinnedIds(newId: string) {
314    if (this.pinnedIds.includes(newId)) {
315      this.pinnedIds = this.pinnedIds.filter((pinned) => pinned !== newId);
316    } else {
317      this.pinnedIds.push(newId);
318    }
319  }
320
321  private getTreeWithTransformedProperties(selectedTree: HierarchyTreeNode): PropertiesTreeNode {
322    const transformer = new TreeTransformer(selectedTree, this.propertiesFilter)
323      .setOnlyProtoDump(true)
324      .setIsShowDefaults(this.propertiesUserOptions['showDefaults']?.enabled)
325      .setIsShowDiff(this.propertiesUserOptions['showDiff']?.enabled)
326      .setTransformerOptions({skip: selectedTree.skip})
327      .setProperties(this.entry)
328      .setDiffProperties(this.previousEntry);
329    this.selectedLayer = transformer.getOriginalFlickerItem(this.entry, selectedTree.stableId);
330    const transformedTree = transformer.transform();
331    return transformedTree;
332  }
333
334  private shouldDisplayPropertyGroups(selectedLayer: Layer): boolean {
335    // Do not display property groups when the root layer is selected. The root layer doesn't
336    // provide property groups info (visibility, geometry transforms, ...).
337    const isRoot = selectedLayer === this.entry;
338    return !isRoot;
339  }
340
341  private copyUiDataAndNotifyView() {
342    // Create a shallow copy of the data, otherwise the Angular OnPush change detection strategy
343    // won't detect the new input
344    const copy = Object.assign({}, this.uiData);
345    this.notifyViewCallback(copy);
346  }
347}
348