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