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