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 {parseMap, stringifyMap} from 'common/persistent_store_proxy'; 20import {Store} from 'common/store'; 21import { 22 TracePositionUpdate, 23 WinscopeEvent, 24 WinscopeEventType, 25} from 'messaging/winscope_event'; 26import { 27 EmitEvent, 28 WinscopeEventEmitter, 29} from 'messaging/winscope_event_emitter'; 30import {Trace, TraceEntry} from 'trace/trace'; 31import {Traces} from 'trace/traces'; 32import {TraceEntryFinder} from 'trace/trace_entry_finder'; 33import {TraceType} from 'trace/trace_type'; 34import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node'; 35import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; 36import {PropertiesPresenter} from 'viewers/common/properties_presenter'; 37import {RectsPresenter} from 'viewers/common/rects_presenter'; 38import {TextFilter} from 'viewers/common/text_filter'; 39import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node'; 40import {UserOptions} from 'viewers/common/user_options'; 41import {HierarchyPresenter} from './hierarchy_presenter'; 42import {PresetHierarchy, TextFilterValues} from './preset_hierarchy'; 43import {RectShowState} from './rect_show_state'; 44import {UiDataHierarchy} from './ui_data_hierarchy'; 45import {ViewerEvents} from './viewer_events'; 46 47export type NotifyHierarchyViewCallbackType<UiData> = (uiData: UiData) => void; 48 49export abstract class AbstractHierarchyViewerPresenter< 50 UiData extends UiDataHierarchy, 51> implements WinscopeEventEmitter 52{ 53 protected emitWinscopeEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC; 54 protected overridePropertiesTree: PropertyTreeNode | undefined; 55 protected overridePropertiesTreeName: string | undefined; 56 protected rectsPresenter?: RectsPresenter; 57 protected abstract hierarchyPresenter: HierarchyPresenter; 58 protected abstract propertiesPresenter: PropertiesPresenter; 59 protected abstract readonly multiTraceType?: TraceType; 60 private highlightedItem = ''; 61 62 constructor( 63 private readonly trace: Trace<HierarchyTreeNode> | undefined, 64 protected readonly traces: Traces, 65 protected readonly storage: Readonly<Store>, 66 private readonly notifyViewCallback: NotifyHierarchyViewCallbackType<UiData>, 67 protected readonly uiData: UiData, 68 ) { 69 uiData.isDarkMode = storage.get('dark-mode') === 'true'; 70 this.copyUiDataAndNotifyView(); 71 } 72 73 setEmitEvent(callback: EmitEvent) { 74 this.emitWinscopeEvent = callback; 75 } 76 77 addEventListeners(htmlElement: HTMLElement) { 78 htmlElement.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => 79 this.onPinnedItemChange((event as CustomEvent).detail.pinnedItem), 80 ); 81 htmlElement.addEventListener( 82 ViewerEvents.HighlightedIdChange, 83 async (event) => 84 await this.onHighlightedIdChange((event as CustomEvent).detail.id), 85 ); 86 htmlElement.addEventListener( 87 ViewerEvents.HighlightedPropertyChange, 88 (event) => 89 this.onHighlightedPropertyChange((event as CustomEvent).detail.id), 90 ); 91 htmlElement.addEventListener( 92 ViewerEvents.HierarchyUserOptionsChange, 93 async (event) => 94 await this.onHierarchyUserOptionsChange( 95 (event as CustomEvent).detail.userOptions, 96 ), 97 ); 98 htmlElement.addEventListener( 99 ViewerEvents.HierarchyFilterChange, 100 async (event) => { 101 const detail: TextFilter = (event as CustomEvent).detail; 102 await this.onHierarchyFilterChange(detail); 103 }, 104 ); 105 htmlElement.addEventListener( 106 ViewerEvents.PropertiesUserOptionsChange, 107 async (event) => 108 await this.onPropertiesUserOptionsChange( 109 (event as CustomEvent).detail.userOptions, 110 ), 111 ); 112 htmlElement.addEventListener( 113 ViewerEvents.PropertiesFilterChange, 114 async (event) => { 115 const detail: TextFilter = (event as CustomEvent).detail; 116 await this.onPropertiesFilterChange(detail); 117 }, 118 ); 119 htmlElement.addEventListener( 120 ViewerEvents.HighlightedNodeChange, 121 async (event) => 122 await this.onHighlightedNodeChange((event as CustomEvent).detail.node), 123 ); 124 htmlElement.addEventListener( 125 ViewerEvents.RectShowStateChange, 126 async (event) => { 127 await this.onRectShowStateChange( 128 (event as CustomEvent).detail.rectId, 129 (event as CustomEvent).detail.state, 130 ); 131 }, 132 ); 133 htmlElement.addEventListener( 134 ViewerEvents.RectsUserOptionsChange, 135 (event) => { 136 this.onRectsUserOptionsChange( 137 (event as CustomEvent).detail.userOptions, 138 ); 139 }, 140 ); 141 } 142 143 onPinnedItemChange(pinnedItem: UiHierarchyTreeNode) { 144 this.hierarchyPresenter.applyPinnedItemChange(pinnedItem); 145 this.uiData.pinnedItems = this.hierarchyPresenter.getPinnedItems(); 146 this.copyUiDataAndNotifyView(); 147 } 148 149 onHighlightedPropertyChange(id: string) { 150 this.propertiesPresenter.applyHighlightedPropertyChange(id); 151 this.uiData.highlightedProperty = 152 this.propertiesPresenter.getHighlightedProperty(); 153 this.copyUiDataAndNotifyView(); 154 } 155 156 onRectsUserOptionsChange(userOptions: UserOptions) { 157 if (!this.rectsPresenter) { 158 return; 159 } 160 this.rectsPresenter.applyRectsUserOptionsChange(userOptions); 161 162 this.uiData.rectsUserOptions = this.rectsPresenter.getUserOptions(); 163 this.uiData.rectsToDraw = this.rectsPresenter.getRectsToDraw(); 164 this.uiData.rectIdToShowState = this.rectsPresenter.getRectIdToShowState(); 165 166 this.copyUiDataAndNotifyView(); 167 } 168 169 async onHierarchyUserOptionsChange(userOptions: UserOptions) { 170 await this.hierarchyPresenter.applyHierarchyUserOptionsChange(userOptions); 171 this.uiData.hierarchyUserOptions = this.hierarchyPresenter.getUserOptions(); 172 this.uiData.hierarchyTrees = this.hierarchyPresenter.getAllFormattedTrees(); 173 this.uiData.pinnedItems = this.hierarchyPresenter.getPinnedItems(); 174 this.copyUiDataAndNotifyView(); 175 } 176 177 async onHierarchyFilterChange(textFilter: TextFilter) { 178 await this.hierarchyPresenter.applyHierarchyFilterChange(textFilter); 179 this.uiData.hierarchyTrees = this.hierarchyPresenter.getAllFormattedTrees(); 180 this.uiData.pinnedItems = this.hierarchyPresenter.getPinnedItems(); 181 this.copyUiDataAndNotifyView(); 182 } 183 184 async onPropertiesUserOptionsChange(userOptions: UserOptions) { 185 this.propertiesPresenter.applyPropertiesUserOptionsChange(userOptions); 186 await this.updatePropertiesTree(); 187 this.uiData.propertiesUserOptions = 188 this.propertiesPresenter.getUserOptions(); 189 this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree(); 190 this.copyUiDataAndNotifyView(); 191 } 192 193 async onPropertiesFilterChange(textFilter: TextFilter) { 194 this.propertiesPresenter.applyPropertiesFilterChange(textFilter); 195 await this.updatePropertiesTree(); 196 this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree(); 197 this.copyUiDataAndNotifyView(); 198 } 199 200 async onRectShowStateChange(id: string, newShowState: RectShowState) { 201 if (!this.rectsPresenter) { 202 return; 203 } 204 this.rectsPresenter.applyRectShowStateChange(id, newShowState); 205 206 this.uiData.rectsToDraw = this.rectsPresenter.getRectsToDraw(); 207 this.uiData.rectIdToShowState = this.rectsPresenter.getRectIdToShowState(); 208 this.copyUiDataAndNotifyView(); 209 } 210 211 async onAppEvent(event: WinscopeEvent) { 212 await event.visit( 213 WinscopeEventType.TRACE_POSITION_UPDATE, 214 async (event) => { 215 if (this.initializeIfNeeded) await this.initializeIfNeeded(event); 216 await this.applyTracePositionUpdate(event); 217 if (this.processDataAfterPositionUpdate) { 218 await this.processDataAfterPositionUpdate(event); 219 } 220 this.refreshUIData(); 221 }, 222 ); 223 await event.visit( 224 WinscopeEventType.FILTER_PRESET_SAVE_REQUEST, 225 async (event) => { 226 this.saveConfigAsPreset(event.name); 227 }, 228 ); 229 await event.visit(WinscopeEventType.DARK_MODE_TOGGLED, async (event) => { 230 this.uiData.isDarkMode = event.isDarkMode; 231 this.copyUiDataAndNotifyView(); 232 }); 233 await event.visit( 234 WinscopeEventType.FILTER_PRESET_APPLY_REQUEST, 235 async (event) => { 236 const filterPresetName = event.name; 237 await this.applyPresetConfig(filterPresetName); 238 this.refreshUIData(); 239 }, 240 ); 241 } 242 243 protected saveConfigAsPreset(storeKey: string) { 244 const preset: PresetHierarchy = { 245 hierarchyUserOptions: this.uiData.hierarchyUserOptions, 246 hierarchyFilter: TextFilterValues.fromTextFilter( 247 this.uiData.hierarchyFilter, 248 ), 249 propertiesUserOptions: this.uiData.propertiesUserOptions, 250 propertiesFilter: TextFilterValues.fromTextFilter( 251 this.uiData.propertiesFilter, 252 ), 253 rectsUserOptions: this.uiData.rectsUserOptions, 254 rectIdToShowState: this.uiData.rectIdToShowState, 255 }; 256 this.storage.add(storeKey, JSON.stringify(preset, stringifyMap)); 257 } 258 259 protected async applyPresetConfig(storeKey: string) { 260 const preset = this.storage.get(storeKey); 261 if (preset) { 262 const parsedPreset: PresetHierarchy = JSON.parse(preset, parseMap); 263 await this.hierarchyPresenter.applyHierarchyUserOptionsChange( 264 parsedPreset.hierarchyUserOptions, 265 ); 266 await this.hierarchyPresenter.applyHierarchyFilterChange( 267 new TextFilter( 268 parsedPreset.hierarchyFilter.filterString, 269 parsedPreset.hierarchyFilter.flags, 270 ), 271 ); 272 273 this.propertiesPresenter.applyPropertiesUserOptionsChange( 274 parsedPreset.propertiesUserOptions, 275 ); 276 this.propertiesPresenter.applyPropertiesFilterChange( 277 new TextFilter( 278 parsedPreset.propertiesFilter.filterString, 279 parsedPreset.propertiesFilter.flags, 280 ), 281 ); 282 await this.updatePropertiesTree(); 283 284 if (this.rectsPresenter) { 285 this.rectsPresenter?.applyRectsUserOptionsChange( 286 assertDefined(parsedPreset.rectsUserOptions), 287 ); 288 this.rectsPresenter?.updateRectShowStates( 289 parsedPreset.rectIdToShowState, 290 ); 291 } 292 this.refreshHierarchyViewerUiData(); 293 } 294 } 295 296 protected async applyTracePositionUpdate(event: TracePositionUpdate) { 297 let entries: Array<TraceEntry<HierarchyTreeNode>> = []; 298 if (this.multiTraceType !== undefined) { 299 entries = this.traces 300 .getTraces(this.multiTraceType) 301 .map((trace) => { 302 return TraceEntryFinder.findCorrespondingEntry( 303 trace, 304 event.position, 305 ) as TraceEntry<HierarchyTreeNode> | undefined; 306 }) 307 .filter((entry) => entry !== undefined) as Array< 308 TraceEntry<HierarchyTreeNode> 309 >; 310 } else { 311 const entry = TraceEntryFinder.findCorrespondingEntry( 312 assertDefined(this.trace), 313 event.position, 314 ); 315 if (entry) entries.push(entry); 316 } 317 318 try { 319 await this.hierarchyPresenter.applyTracePositionUpdate( 320 entries, 321 this.highlightedItem, 322 ); 323 } catch (e) { 324 this.hierarchyPresenter.clear(); 325 this.rectsPresenter?.clear(); 326 this.propertiesPresenter.clear(); 327 this.refreshHierarchyViewerUiData(); 328 throw e; 329 } 330 331 const propertiesOpts = this.propertiesPresenter.getUserOptions(); 332 const hasPreviousEntry = entries.some((e) => e.getIndex() > 0); 333 if (propertiesOpts['showDiff']?.isUnavailable !== undefined) { 334 propertiesOpts['showDiff'].isUnavailable = !hasPreviousEntry; 335 } 336 337 const currentHierarchyTrees = 338 this.hierarchyPresenter.getAllCurrentHierarchyTrees(); 339 if (currentHierarchyTrees) { 340 this.rectsPresenter?.applyHierarchyTreesChange(currentHierarchyTrees); 341 await this.updatePropertiesTree(); 342 } 343 } 344 345 protected async applyHighlightedNodeChange(node: UiHierarchyTreeNode) { 346 this.updateHighlightedItem(node.id); 347 this.hierarchyPresenter.applyHighlightedNodeChange(node); 348 await this.updatePropertiesTree(); 349 } 350 351 protected async applyHighlightedIdChange(newId: string) { 352 this.updateHighlightedItem(newId); 353 this.hierarchyPresenter.applyHighlightedIdChange(newId); 354 await this.updatePropertiesTree(); 355 } 356 357 protected async updatePropertiesTree() { 358 if (this.overridePropertiesTree) { 359 this.propertiesPresenter.setPropertiesTree(this.overridePropertiesTree); 360 await this.propertiesPresenter.formatPropertiesTree( 361 undefined, 362 this.overridePropertiesTreeName, 363 false, 364 ); 365 return; 366 } 367 const selected = this.hierarchyPresenter.getSelectedTree(); 368 if (selected) { 369 const [trace, selectedTree] = selected; 370 const propertiesTree = await selectedTree.getAllProperties(); 371 if ( 372 this.propertiesPresenter.getUserOptions()['showDiff']?.enabled && 373 !this.hierarchyPresenter.getPreviousHierarchyTreeForTrace(trace) 374 ) { 375 await this.hierarchyPresenter.updatePreviousHierarchyTrees(); 376 } 377 const previousTree = 378 this.hierarchyPresenter.getPreviousHierarchyTreeForTrace(trace); 379 this.propertiesPresenter.setPropertiesTree(propertiesTree); 380 await this.propertiesPresenter.formatPropertiesTree( 381 previousTree, 382 this.getOverrideDisplayName(selected), 383 this.keepCalculated(selectedTree), 384 ); 385 } else { 386 this.propertiesPresenter.clear(); 387 } 388 } 389 390 protected updateHighlightedItem(id: string) { 391 if (this.highlightedItem === id) { 392 this.highlightedItem = ''; 393 } else { 394 this.highlightedItem = id; 395 } 396 } 397 398 protected refreshHierarchyViewerUiData() { 399 this.uiData.highlightedItem = this.highlightedItem; 400 this.uiData.pinnedItems = this.hierarchyPresenter.getPinnedItems(); 401 this.uiData.hierarchyUserOptions = this.hierarchyPresenter.getUserOptions(); 402 this.uiData.hierarchyTrees = this.hierarchyPresenter.getAllFormattedTrees(); 403 this.uiData.hierarchyFilter = this.hierarchyPresenter.getTextFilter(); 404 405 this.uiData.propertiesUserOptions = 406 this.propertiesPresenter.getUserOptions(); 407 this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree(); 408 this.uiData.highlightedProperty = 409 this.propertiesPresenter.getHighlightedProperty(); 410 this.uiData.propertiesFilter = assertDefined( 411 this.propertiesPresenter.getTextFilter(), 412 ); 413 414 if (this.rectsPresenter) { 415 this.uiData.rectsToDraw = this.rectsPresenter?.getRectsToDraw(); 416 this.uiData.rectIdToShowState = 417 this.rectsPresenter.getRectIdToShowState(); 418 this.uiData.displays = this.rectsPresenter.getDisplays(); 419 this.uiData.rectsUserOptions = this.rectsPresenter.getUserOptions(); 420 } 421 422 this.copyUiDataAndNotifyView(); 423 } 424 425 protected getHighlightedItem(): string | undefined { 426 return this.highlightedItem; 427 } 428 429 protected getEntryFormattedTimestamp( 430 entry: TraceEntry<HierarchyTreeNode>, 431 ): string { 432 if (entry.getFullTrace().isDumpWithoutTimestamp()) { 433 return 'Dump'; 434 } 435 return entry.getTimestamp().format(); 436 } 437 438 private copyUiDataAndNotifyView() { 439 // Create a shallow copy of the data, otherwise the Angular OnPush change detection strategy 440 // won't detect the new input 441 const copy = Object.assign({}, this.uiData); 442 this.notifyViewCallback(copy); 443 } 444 445 abstract onHighlightedNodeChange(node: UiHierarchyTreeNode): Promise<void>; 446 abstract onHighlightedIdChange(id: string): Promise<void>; 447 protected abstract keepCalculated(tree: HierarchyTreeNode): boolean; 448 protected abstract getOverrideDisplayName( 449 selected: [Trace<HierarchyTreeNode>, HierarchyTreeNode], 450 ): string | undefined; 451 protected abstract refreshUIData(): void; 452 protected initializeIfNeeded?(event: TracePositionUpdate): Promise<void>; 453 protected processDataAfterPositionUpdate?( 454 event: TracePositionUpdate, 455 ): Promise<void>; 456} 457