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