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/store/persistent_store_proxy'; 19import {Store} from 'common/store/store'; 20import { 21 TabbedViewSwitchRequest, 22 TracePositionUpdate, 23} from 'messaging/winscope_event'; 24import {LayerFlag} from 'parsers/surface_flinger/layer_flag'; 25import {CustomQueryType} from 'trace/custom_query'; 26import {Trace} from 'trace/trace'; 27import {Traces} from 'trace/traces'; 28import {TraceEntryFinder} from 'trace/trace_entry_finder'; 29import {TRACE_INFO} from 'trace/trace_info'; 30import {TraceType} from 'trace/trace_type'; 31import { 32 EMPTY_OBJ_STRING, 33 FixedStringFormatter, 34} from 'trace/tree_node/formatters'; 35import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node'; 36import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; 37import { 38 AbstractHierarchyViewerPresenter, 39 NotifyHierarchyViewCallbackType, 40} from 'viewers/common/abstract_hierarchy_viewer_presenter'; 41import {VISIBLE_CHIP} from 'viewers/common/chip'; 42import { 43 SfCuratedProperties, 44 SfLayerSummary, 45 SfSummaryProperty, 46} from 'viewers/common/curated_properties'; 47import {DisplayIdentifier} from 'viewers/common/display_identifier'; 48import { 49 HierarchyPresenter, 50 SelectedTree, 51} from 'viewers/common/hierarchy_presenter'; 52import {PropertiesPresenter} from 'viewers/common/properties_presenter'; 53import {RectsPresenter} from 'viewers/common/rects_presenter'; 54import {TextFilter} from 'viewers/common/text_filter'; 55import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node'; 56import {UI_RECT_FACTORY} from 'viewers/common/ui_rect_factory'; 57import {UserOptions} from 'viewers/common/user_options'; 58import {ViewerEvents} from 'viewers/common/viewer_events'; 59import { 60 RectLegendFactory, 61 RectSpec, 62 TraceRectType, 63} from 'viewers/components/rects/rect_spec'; 64import {UiRect} from 'viewers/components/rects/ui_rect'; 65import {UiData} from './ui_data'; 66 67export class Presenter extends AbstractHierarchyViewerPresenter<UiData> { 68 static readonly DENYLIST_PROPERTY_NAMES = [ 69 'name', 70 'children', 71 'dpiX', 72 'dpiY', 73 ]; 74 75 protected override hierarchyPresenter = new HierarchyPresenter( 76 PersistentStoreProxy.new<UserOptions>( 77 'SfHierarchyOptions', 78 { 79 showDiff: { 80 name: 'Show diff', // TODO: PersistentStoreObject.Ignored("Show diff") or something like that to instruct to not store this info 81 enabled: false, 82 isUnavailable: false, 83 }, 84 showOnlyVisible: { 85 name: 'Show only', 86 chip: VISIBLE_CHIP, 87 enabled: false, 88 }, 89 simplifyNames: { 90 name: 'Simplify names', 91 enabled: true, 92 }, 93 flat: { 94 name: 'Flat', 95 enabled: false, 96 }, 97 }, 98 this.storage, 99 ), 100 new TextFilter(), 101 Presenter.DENYLIST_PROPERTY_NAMES, 102 true, 103 false, 104 this.getEntryFormattedTimestamp, 105 ); 106 protected override rectsPresenter = new RectsPresenter( 107 PersistentStoreProxy.new<UserOptions>( 108 'SfRectsOptions', 109 { 110 ignoreRectShowState: { 111 name: 'Ignore', 112 icon: 'visibility', 113 enabled: false, 114 }, 115 showOnlyVisible: { 116 name: 'Show only', 117 chip: VISIBLE_CHIP, 118 enabled: false, 119 }, 120 }, 121 this.storage, 122 ), 123 (tree: HierarchyTreeNode) => { 124 if (this.rectSpecs[this.rectSpecIndex].type === TraceRectType.LAYERS) { 125 return UI_RECT_FACTORY.makeUiRects(tree, this.viewCapturePackageNames); 126 } 127 return UI_RECT_FACTORY.makeInputRects(tree, (id) => false); 128 }, 129 (displays: UiRect[]) => 130 makeDisplayIdentifiers(displays, this.wmFocusedDisplayId), 131 convertRectIdToLayerorDisplayName, 132 ); 133 protected override propertiesPresenter = new PropertiesPresenter( 134 PersistentStoreProxy.new<UserOptions>( 135 'SfPropertyOptions', 136 { 137 showDiff: { 138 name: 'Show diff', 139 enabled: false, 140 isUnavailable: false, 141 }, 142 showDefaults: { 143 name: 'Show defaults', 144 enabled: false, 145 tooltip: `If checked, shows the value of all properties. 146Otherwise, hides all properties whose value is 147the default for its data type.`, 148 }, 149 }, 150 this.storage, 151 ), 152 new TextFilter(), 153 Presenter.DENYLIST_PROPERTY_NAMES, 154 undefined, 155 ['a', 'type'], 156 ); 157 protected override multiTraceType = undefined; 158 159 private viewCapturePackageNames: string[] | undefined; 160 private curatedProperties: SfCuratedProperties | undefined; 161 private wmTrace: Trace<HierarchyTreeNode> | undefined; 162 private wmFocusedDisplayId: number | undefined; 163 private rectSpecs: RectSpec[] = [ 164 { 165 type: TraceRectType.LAYERS, 166 icon: TRACE_INFO[TraceType.SURFACE_FLINGER].icon, 167 legend: RectLegendFactory.makeLegendForLayerRects(true), 168 }, 169 { 170 type: TraceRectType.INPUT_WINDOWS, 171 icon: TRACE_INFO[TraceType.INPUT_EVENT_MERGED].icon, 172 legend: RectLegendFactory.makeLegendForInputWindowRects(true), 173 }, 174 ]; 175 private rectSpecIndex = 0; 176 177 constructor( 178 trace: Trace<HierarchyTreeNode>, 179 traces: Traces, 180 storage: Readonly<Store>, 181 notifyViewCallback: NotifyHierarchyViewCallbackType<UiData>, 182 ) { 183 super(trace, traces, storage, notifyViewCallback, new UiData()); 184 this.uiData.allRectSpecs = this.rectSpecs; 185 this.wmTrace = traces.getTrace(TraceType.WINDOW_MANAGER); 186 } 187 188 async onRectDoubleClick(rectId: string) { 189 if (!this.viewCapturePackageNames) { 190 return; 191 } 192 const rectHasViewCapture = this.viewCapturePackageNames.some( 193 (packageName) => rectId.includes(packageName), 194 ); 195 if (!rectHasViewCapture) { 196 return; 197 } 198 const newActiveTrace = assertDefined( 199 this.traces.getTrace(TraceType.VIEW_CAPTURE), 200 ); 201 await this.emitWinscopeEvent(new TabbedViewSwitchRequest(newActiveTrace)); 202 } 203 204 override async onHighlightedNodeChange(item: UiHierarchyTreeNode) { 205 await this.applyHighlightedNodeChange(item); 206 this.updateCuratedProperties(); 207 this.refreshUIData(); 208 } 209 210 override async onHighlightedIdChange(newId: string) { 211 await this.applyHighlightedIdChange(newId); 212 this.updateCuratedProperties(); 213 this.refreshUIData(); 214 } 215 216 onRectTypeButtonClicked(type: TraceRectType) { 217 this.rectSpecIndex = this.rectSpecs.findIndex((spec) => spec.type === type); 218 const currentHierarchyTrees = 219 this.hierarchyPresenter.getAllCurrentHierarchyTrees(); 220 if (currentHierarchyTrees) { 221 this.rectsPresenter?.applyHierarchyTreesChange(currentHierarchyTrees); 222 } 223 this.refreshUIData(); 224 } 225 226 protected override getOverrideDisplayName( 227 selected: SelectedTree, 228 ): string | undefined { 229 return selected.tree.isRoot() 230 ? this.hierarchyPresenter 231 .getCurrentHierarchyTreeNames(selected.trace) 232 ?.at(0) 233 : undefined; 234 } 235 236 protected override keepCalculated(tree: HierarchyTreeNode): boolean { 237 return tree.isRoot(); 238 } 239 240 protected override async initializeIfNeeded(event: TracePositionUpdate) { 241 if (!this.viewCapturePackageNames) { 242 const tracesVc = this.traces.getTraces(TraceType.VIEW_CAPTURE); 243 const promisesPackageName = tracesVc.map(async (trace) => { 244 const packageAndWindow = await trace.customQuery( 245 CustomQueryType.VIEW_CAPTURE_METADATA, 246 ); 247 return packageAndWindow.packageName; 248 }); 249 this.viewCapturePackageNames = await Promise.all(promisesPackageName); 250 } 251 await this.setInitialWmActiveDisplay(event); 252 } 253 254 protected override async processDataAfterPositionUpdate(): Promise<void> { 255 this.updateCuratedProperties(); 256 } 257 258 protected override refreshUIData() { 259 this.uiData.curatedProperties = this.curatedProperties; 260 this.uiData.rectSpec = this.rectSpecs[this.rectSpecIndex]; 261 this.refreshHierarchyViewerUiData(); 262 } 263 264 protected override addViewerSpecificListeners(htmlElement: HTMLElement) { 265 htmlElement.addEventListener(ViewerEvents.RectsDblClick, async (event) => { 266 const rectId = (event as CustomEvent).detail.clickedRectId; 267 await this.onRectDoubleClick(rectId); 268 }); 269 htmlElement.addEventListener(ViewerEvents.RectTypeButtonClick, (event) => { 270 const type = (event as CustomEvent).detail.type; 271 this.onRectTypeButtonClicked(type); 272 }); 273 } 274 275 private updateCuratedProperties() { 276 const selectedHierarchyTree = this.hierarchyPresenter.getSelectedTree(); 277 const propertiesTree = this.propertiesPresenter.getPropertiesTree(); 278 279 if (selectedHierarchyTree && propertiesTree) { 280 if (selectedHierarchyTree.tree.isRoot()) { 281 this.curatedProperties = undefined; 282 } else { 283 this.curatedProperties = this.getCuratedProperties( 284 selectedHierarchyTree.tree, 285 propertiesTree, 286 ); 287 } 288 } else { 289 this.curatedProperties = undefined; 290 } 291 } 292 293 private getCuratedProperties( 294 hTree: HierarchyTreeNode, 295 pTree: PropertyTreeNode, 296 ): SfCuratedProperties { 297 const inputWindowInfo = pTree.getChildByName('inputWindowInfo'); 298 const hasInputChannel = 299 inputWindowInfo !== undefined && 300 inputWindowInfo.getAllChildren().length > 0; 301 302 const cropLayerId = hasInputChannel 303 ? assertDefined( 304 inputWindowInfo.getChildByName('cropLayerId'), 305 ).formattedValue() 306 : '-1'; 307 308 const verboseFlags = pTree.getChildByName('verboseFlags')?.formattedValue(); 309 const flags = assertDefined(pTree.getChildByName('flags')); 310 const curatedFlags = 311 verboseFlags !== '' && verboseFlags !== undefined 312 ? verboseFlags 313 : flags.formattedValue(); 314 315 const bufferTransform = pTree.getChildByName('bufferTransform'); 316 const bufferTransformTypeFlags = 317 bufferTransform?.getChildByName('type')?.formattedValue() ?? 'null'; 318 319 const zOrderRelativeOfNode = assertDefined( 320 pTree.getChildByName('zOrderRelativeOf'), 321 ); 322 let relativeParent: string | SfLayerSummary = 323 zOrderRelativeOfNode.formattedValue(); 324 if (relativeParent !== 'none') { 325 // update zOrderRelativeOf property formatter to zParent node id 326 zOrderRelativeOfNode.setFormatter( 327 new FixedStringFormatter(assertDefined(hTree.getZParent()).id), 328 ); 329 relativeParent = this.getLayerSummary( 330 zOrderRelativeOfNode.formattedValue(), 331 ); 332 } 333 334 const curated: SfCuratedProperties = { 335 summary: this.getSummaryOfVisibility(pTree), 336 flags: curatedFlags, 337 calcTransform: pTree.getChildByName('transform'), 338 calcCrop: this.getCropPropertyValue(pTree, 'bounds'), 339 finalBounds: assertDefined( 340 pTree.getChildByName('screenBounds'), 341 ).formattedValue(), 342 reqTransform: pTree.getChildByName('requestedTransform'), 343 bufferSize: assertDefined( 344 pTree.getChildByName('activeBuffer'), 345 ).formattedValue(), 346 frameNumber: assertDefined( 347 pTree.getChildByName('currFrame'), 348 ).formattedValue(), 349 bufferTransformType: bufferTransformTypeFlags, 350 destinationFrame: assertDefined( 351 pTree.getChildByName('destinationFrame'), 352 ).formattedValue(), 353 z: assertDefined(pTree.getChildByName('z')).formattedValue(), 354 relativeParent, 355 relativeChildren: hTree 356 .getRelativeChildren() 357 .map((c) => this.getLayerSummary(c.id)), 358 calcColor: this.getColorPropertyValue(pTree, 'color'), 359 calcShadowRadius: this.getPixelPropertyValue(pTree, 'shadowRadius'), 360 calcCornerRadius: this.getPixelPropertyValue(pTree, 'cornerRadius'), 361 calcCornerRadiusCrop: this.getCropPropertyValue( 362 pTree, 363 'cornerRadiusCrop', 364 ), 365 backgroundBlurRadius: this.getPixelPropertyValue( 366 pTree, 367 'backgroundBlurRadius', 368 ), 369 reqColor: this.getColorPropertyValue(pTree, 'requestedColor'), 370 reqCornerRadius: this.getPixelPropertyValue( 371 pTree, 372 'requestedCornerRadius', 373 ), 374 inputTransform: inputWindowInfo?.getChildByName('transform'), 375 inputRegion: inputWindowInfo 376 ?.getChildByName('touchableRegion') 377 ?.formattedValue(), 378 focusable: hasInputChannel 379 ? assertDefined( 380 inputWindowInfo.getChildByName('focusable'), 381 ).formattedValue() 382 : 'null', 383 cropTouchRegionWithItem: cropLayerId, 384 replaceTouchRegionWithCrop: hasInputChannel 385 ? inputWindowInfo 386 .getChildByName('replaceTouchableRegionWithCrop') 387 ?.formattedValue() ?? 'false' 388 : 'false', 389 inputConfig: 390 inputWindowInfo?.getChildByName('inputConfig')?.formattedValue() ?? 391 'null', 392 ignoreDestinationFrame: 393 (flags.getValue() & LayerFlag.IGNORE_DESTINATION_FRAME) === 394 LayerFlag.IGNORE_DESTINATION_FRAME, 395 hasInputChannel, 396 }; 397 return curated; 398 } 399 400 private getSummaryOfVisibility(tree: PropertyTreeNode): SfSummaryProperty[] { 401 const summary: SfSummaryProperty[] = []; 402 const visibilityReason = tree.getChildByName('visibilityReason'); 403 if (visibilityReason && visibilityReason.getAllChildren().length > 0) { 404 const reason = this.mapNodeArrayToString( 405 visibilityReason.getAllChildren(), 406 ); 407 summary.push({key: 'Invisible due to', simpleValue: reason}); 408 } 409 410 const occludedBy = tree.getChildByName('occludedBy')?.getAllChildren(); 411 if (occludedBy && occludedBy.length > 0) { 412 summary.push({ 413 key: 'Occluded by', 414 layerValues: occludedBy.map((layer) => 415 this.getLayerSummary(layer.formattedValue()), 416 ), 417 desc: 'Fully occluded by these opaque layers', 418 }); 419 } 420 421 const partiallyOccludedBy = tree 422 .getChildByName('partiallyOccludedBy') 423 ?.getAllChildren(); 424 if (partiallyOccludedBy && partiallyOccludedBy.length > 0) { 425 summary.push({ 426 key: 'Partially occluded by', 427 layerValues: partiallyOccludedBy.map((layer) => 428 this.getLayerSummary(layer.formattedValue()), 429 ), 430 desc: 'Partially occluded by these opaque layers', 431 }); 432 } 433 434 const coveredBy = tree.getChildByName('coveredBy')?.getAllChildren(); 435 if (coveredBy && coveredBy.length > 0) { 436 summary.push({ 437 key: 'Covered by', 438 layerValues: coveredBy.map((layer) => 439 this.getLayerSummary(layer.formattedValue()), 440 ), 441 desc: 'Partially or fully covered by these likely translucent layers', 442 }); 443 } 444 return summary; 445 } 446 447 private mapNodeArrayToString(nodes: readonly PropertyTreeNode[]): string { 448 return nodes.map((reason) => reason.formattedValue()).join(', '); 449 } 450 451 private getLayerSummary(nodeId: string): SfLayerSummary { 452 const parts = nodeId.split(' '); 453 return { 454 layerId: parts[0], 455 nodeId, 456 name: parts.slice(1).join(' '), 457 }; 458 } 459 460 private getPixelPropertyValue(tree: PropertyTreeNode, label: string): string { 461 const propVal = assertDefined(tree.getChildByName(label)).formattedValue(); 462 return propVal !== 'null' ? `${propVal} px` : '0 px'; 463 } 464 465 private getCropPropertyValue(tree: PropertyTreeNode, label: string): string { 466 const propVal = assertDefined(tree.getChildByName(label)).formattedValue(); 467 return propVal !== 'null' ? propVal : EMPTY_OBJ_STRING; 468 } 469 470 private getColorPropertyValue(tree: PropertyTreeNode, label: string): string { 471 const propVal = assertDefined(tree.getChildByName(label)).formattedValue(); 472 return propVal !== 'null' ? propVal : 'no color found'; 473 } 474 475 private async setInitialWmActiveDisplay(event: TracePositionUpdate) { 476 if (!this.wmTrace || this.wmFocusedDisplayId !== undefined) { 477 return; 478 } 479 const wmEntry: HierarchyTreeNode | undefined = 480 await TraceEntryFinder.findCorrespondingEntry<HierarchyTreeNode>( 481 this.wmTrace, 482 event.position, 483 )?.getValue(); 484 if (wmEntry) { 485 this.wmFocusedDisplayId = wmEntry 486 .getEagerPropertyByName('focusedDisplayId') 487 ?.getValue(); 488 } 489 } 490} 491 492export function makeDisplayIdentifiers( 493 rects: UiRect[], 494 focusedDisplayId?: number, 495): DisplayIdentifier[] { 496 const ids: DisplayIdentifier[] = []; 497 498 const isActive = (display: UiRect) => { 499 if (focusedDisplayId !== undefined) { 500 return display.groupId === focusedDisplayId; 501 } 502 return display.isActiveDisplay; 503 }; 504 505 rects.forEach((rect: UiRect) => { 506 if (!rect.isDisplay) return; 507 508 const displayId = rect.id.slice(10, rect.id.length); 509 ids.push({ 510 displayId, 511 groupId: rect.groupId, 512 name: rect.label, 513 isActive: isActive(rect), 514 }); 515 }); 516 517 let offscreenDisplayCount = 0; 518 rects.forEach((rect: UiRect) => { 519 if (rect.isDisplay) return; 520 521 if (!ids.find((identifier) => identifier.groupId === rect.groupId)) { 522 offscreenDisplayCount++; 523 const name = 524 'Offscreen Display' + 525 (offscreenDisplayCount > 1 ? ` ${offscreenDisplayCount}` : ''); 526 ids.push({displayId: -1, groupId: rect.groupId, name, isActive: false}); 527 } 528 }); 529 530 return ids; 531} 532 533export function convertRectIdToLayerorDisplayName(id: string) { 534 if (id.startsWith('Display')) return id.split('-').slice(1).join('-').trim(); 535 const idMinusStartLayerId = id.split(' ').slice(1).join(' '); 536 const idSplittingEndLayerId = idMinusStartLayerId.split('#'); 537 return idSplittingEndLayerId 538 .slice(0, idSplittingEndLayerId.length - 1) 539 .join('#'); 540} 541