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 {PersistentStoreProxy} from 'common/store/persistent_store_proxy'; 19import {Store} from 'common/store/store'; 20import {Analytics} from 'logging/analytics'; 21import {TabbedViewSwitchRequest} from 'messaging/winscope_event'; 22import {CustomQueryType} from 'trace/custom_query'; 23import {Trace, TraceEntry, TraceEntryLazy} from 'trace/trace'; 24import {Traces} from 'trace/traces'; 25import {TRACE_INFO} from 'trace/trace_info'; 26import {TraceType} from 'trace/trace_type'; 27import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node'; 28import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; 29import { 30 AbstractLogViewerPresenter, 31 NotifyLogViewCallbackType, 32} from 'viewers/common/abstract_log_viewer_presenter'; 33import {VISIBLE_CHIP} from 'viewers/common/chip'; 34import {LogSelectFilter} from 'viewers/common/log_filters'; 35import {LogPresenter} from 'viewers/common/log_presenter'; 36import {PropertiesPresenter} from 'viewers/common/properties_presenter'; 37import {RectsPresenter} from 'viewers/common/rects_presenter'; 38import {TextFilter} from 'viewers/common/text_filter'; 39import {ColumnSpec, LogEntry, LogHeader} from 'viewers/common/ui_data_log'; 40import {UI_RECT_FACTORY} from 'viewers/common/ui_rect_factory'; 41import {UserOptions} from 'viewers/common/user_options'; 42import {ViewerEvents} from 'viewers/common/viewer_events'; 43import { 44 RectLegendFactory, 45 TraceRectType, 46} from 'viewers/components/rects/rect_spec'; 47import { 48 convertRectIdToLayerorDisplayName, 49 makeDisplayIdentifiers, 50} from 'viewers/viewer_surface_flinger/presenter'; 51import {DispatchEntryFormatter} from './operations/dispatch_entry_formatter'; 52import {InputCoordinatePropagator} from './operations/input_coordinate_propagator'; 53import {InputEntry, UiData} from './ui_data'; 54 55enum InputEventType { 56 KEY, 57 MOTION, 58} 59 60export class Presenter extends AbstractLogViewerPresenter< 61 UiData, 62 PropertyTreeNode 63> { 64 private static readonly COLUMNS = { 65 type: { 66 name: 'Type', 67 cssClass: 'input-type inline', 68 }, 69 source: { 70 name: 'Source', 71 cssClass: 'input-source', 72 }, 73 action: { 74 name: 'Action', 75 cssClass: 'input-action', 76 }, 77 deviceId: { 78 name: 'Device', 79 cssClass: 'input-device-id right-align', 80 }, 81 displayId: { 82 name: 'Display', 83 cssClass: 'input-display-id right-align', 84 }, 85 details: { 86 name: 'Details', 87 cssClass: 'input-details', 88 }, 89 dispatchWindows: { 90 name: 'Target Windows', 91 cssClass: 'input-windows', 92 }, 93 }; 94 static readonly DENYLIST_DISPATCH_PROPERTIES = ['eventId']; 95 96 private readonly traces: Traces; 97 private readonly surfaceFlingerTrace: Trace<HierarchyTreeNode> | undefined; 98 99 private readonly inputCoordinatePropagator = new InputCoordinatePropagator(); 100 101 private readonly layerIdToName = new Map<number, string>(); 102 private readonly allInputLayerIds = new Set<number>(); 103 104 protected override logPresenter = new LogPresenter<InputEntry>(); 105 protected override propertiesPresenter = new PropertiesPresenter( 106 {}, 107 new TextFilter(), 108 [], 109 ); 110 protected dispatchPropertiesPresenter = new PropertiesPresenter( 111 {}, 112 new TextFilter(), 113 Presenter.DENYLIST_DISPATCH_PROPERTIES, 114 [new DispatchEntryFormatter(this.layerIdToName)], 115 ); 116 protected override keepCalculated = true; 117 private readonly currentTargetWindowIds = new Set<string>(); 118 119 private readonly rectsPresenter = new RectsPresenter( 120 PersistentStoreProxy.new<UserOptions>( 121 'InputWindowRectsOptions', 122 { 123 showOnlyWithContent: { 124 name: 'Has input', 125 icon: 'pan_tool_alt', 126 enabled: false, 127 }, 128 showOnlyVisible: { 129 name: 'Show only', 130 chip: VISIBLE_CHIP, 131 enabled: true, 132 }, 133 }, 134 this.storage, 135 ), 136 (tree: HierarchyTreeNode) => 137 UI_RECT_FACTORY.makeInputRects(tree, (id) => 138 this.currentTargetWindowIds.has(id.split(' ')[0]), 139 ), 140 makeDisplayIdentifiers, 141 convertRectIdToLayerorDisplayName, 142 ); 143 144 constructor( 145 traces: Traces, 146 mergedInputEventTrace: Trace<PropertyTreeNode>, 147 private readonly storage: Store, 148 readonly notifyInputViewCallback: NotifyLogViewCallbackType<UiData>, 149 ) { 150 const uiData = UiData.createEmpty(); 151 uiData.isDarkMode = storage.get('dark-mode') === 'true'; 152 uiData.rectSpec = { 153 type: TraceRectType.INPUT_WINDOWS, 154 icon: TRACE_INFO[TraceType.INPUT_EVENT_MERGED].icon, 155 legend: RectLegendFactory.makeLegendForInputWindowRects(false), 156 }; 157 super( 158 mergedInputEventTrace, 159 (uiData) => notifyInputViewCallback(uiData as UiData), 160 uiData, 161 ); 162 this.traces = traces; 163 this.surfaceFlingerTrace = this.traces.getTrace(TraceType.SURFACE_FLINGER); 164 } 165 166 async onDispatchPropertiesFilterChange(textFilter: TextFilter) { 167 this.dispatchPropertiesPresenter.applyPropertiesFilterChange(textFilter); 168 await this.updateDispatchPropertiesTree(); 169 this.uiData.dispatchPropertiesFilter = textFilter; 170 this.notifyViewChanged(); 171 } 172 173 protected override async initializeTraceSpecificData() { 174 if (this.surfaceFlingerTrace !== undefined) { 175 const layerMappings = await this.surfaceFlingerTrace.customQuery( 176 CustomQueryType.SF_LAYERS_ID_AND_NAME, 177 ); 178 layerMappings.forEach(({id, name}) => this.layerIdToName.set(id, name)); 179 } 180 } 181 182 protected override makeHeaders(): LogHeader[] { 183 return [ 184 new LogHeader( 185 Presenter.COLUMNS.type, 186 new LogSelectFilter([], false, '80'), 187 ), 188 new LogHeader( 189 Presenter.COLUMNS.source, 190 new LogSelectFilter([], false, '200'), 191 ), 192 new LogHeader( 193 Presenter.COLUMNS.action, 194 new LogSelectFilter([], false, '100'), 195 ), 196 new LogHeader( 197 Presenter.COLUMNS.deviceId, 198 new LogSelectFilter([], false, '80'), 199 ), 200 new LogHeader( 201 Presenter.COLUMNS.displayId, 202 new LogSelectFilter([], false, '80'), 203 ), 204 new LogHeader(Presenter.COLUMNS.details), 205 new LogHeader( 206 Presenter.COLUMNS.dispatchWindows, 207 new LogSelectFilter([], true, '300'), 208 ), 209 ]; 210 } 211 212 protected override async makeUiDataEntries(): Promise<InputEntry[]> { 213 const entries: InputEntry[] = []; 214 for (let i = 0; i < this.trace.lengthEntries; i++) { 215 const traceEntry = assertDefined(this.trace.getEntry(i)); 216 const entry = await this.makeInputEntry(traceEntry); 217 entries.push(entry); 218 } 219 return Promise.resolve(entries); 220 } 221 222 private static getUniqueFieldValues( 223 headers: LogHeader[], 224 entries: LogEntry[], 225 ): Map<ColumnSpec, Set<string>> { 226 const uniqueFieldValues = new Map<ColumnSpec, Set<string>>(); 227 headers.forEach((header) => { 228 if (!header.filter || header.spec === Presenter.COLUMNS.dispatchWindows) { 229 return; 230 } 231 uniqueFieldValues.set(header.spec, new Set()); 232 }); 233 entries.forEach((entry) => { 234 entry.fields.forEach((field) => { 235 uniqueFieldValues.get(field.spec)?.add(field.value.toString()); 236 }); 237 }); 238 return uniqueFieldValues; 239 } 240 241 protected override updateFiltersInHeaders( 242 headers: LogHeader[], 243 entries: LogEntry[], 244 ) { 245 const uniqueFieldValues = Presenter.getUniqueFieldValues(headers, entries); 246 headers.forEach((header) => { 247 if (!(header.filter instanceof LogSelectFilter)) { 248 return; 249 } 250 if (header.spec === Presenter.COLUMNS.dispatchWindows) { 251 header.filter.options = [...this.allInputLayerIds.values()].map( 252 (layerId) => { 253 return this.getLayerDisplayName(layerId); 254 }, 255 ); 256 return; 257 } 258 header.filter.options = Array.from( 259 assertDefined(uniqueFieldValues.get(header.spec)), 260 ); 261 header.filter.options.sort(); 262 }); 263 } 264 265 private async makeInputEntry( 266 traceEntry: TraceEntryLazy<PropertyTreeNode>, 267 ): Promise<InputEntry> { 268 const wrapperTree = await traceEntry.getValue(); 269 this.inputCoordinatePropagator.apply(wrapperTree); 270 271 let eventTree = wrapperTree.getChildByName('keyEvent'); 272 let type = InputEventType.KEY; 273 if (eventTree === undefined || eventTree.getAllChildren().length === 0) { 274 eventTree = assertDefined(wrapperTree.getChildByName('motionEvent')); 275 type = InputEventType.MOTION; 276 } 277 eventTree.setIsRoot(true); 278 279 const dispatchTree = assertDefined( 280 wrapperTree.getChildByName('windowDispatchEvents'), 281 ); 282 dispatchTree.setIsRoot(true); 283 dispatchTree.getAllChildren().forEach((dispatchEntry) => { 284 const windowIdNode = dispatchEntry.getChildByName('windowId'); 285 const windowId = Number(windowIdNode?.getValue() ?? -1); 286 this.allInputLayerIds.add(windowId); 287 }); 288 289 let sfEntry: TraceEntry<HierarchyTreeNode> | undefined; 290 if (this.surfaceFlingerTrace !== undefined && this.trace.hasFrameInfo()) { 291 const frame = traceEntry.getFramesRange()?.start; 292 if (frame !== undefined) { 293 const sfFrame = this.surfaceFlingerTrace.getFrame(frame); 294 if (sfFrame.lengthEntries > 0) { 295 sfEntry = sfFrame.getEntry(0); 296 } 297 } 298 } 299 300 return new InputEntry( 301 traceEntry, 302 [ 303 { 304 spec: Presenter.COLUMNS.type, 305 value: type === InputEventType.KEY ? 'KEY' : 'MOTION', 306 propagateEntryTimestamp: true, 307 }, 308 { 309 spec: Presenter.COLUMNS.source, 310 value: assertDefined(eventTree.getChildByName('source')) 311 .formattedValue() 312 .replace('SOURCE_', ''), 313 }, 314 { 315 spec: Presenter.COLUMNS.action, 316 value: assertDefined(eventTree.getChildByName('action')) 317 .formattedValue() 318 .replace('ACTION_', ''), 319 }, 320 { 321 spec: Presenter.COLUMNS.deviceId, 322 value: assertDefined(eventTree.getChildByName('deviceId')).getValue(), 323 }, 324 { 325 spec: Presenter.COLUMNS.displayId, 326 value: assertDefined( 327 eventTree.getChildByName('displayId'), 328 ).getValue(), 329 }, 330 { 331 spec: Presenter.COLUMNS.details, 332 value: 333 type === InputEventType.KEY 334 ? Presenter.extractKeyDetails(eventTree, dispatchTree) 335 : Presenter.extractDispatchDetails(dispatchTree), 336 }, 337 { 338 spec: Presenter.COLUMNS.dispatchWindows, 339 value: dispatchTree 340 .getAllChildren() 341 .map((dispatchEntry) => { 342 const windowId = Number( 343 dispatchEntry.getChildByName('windowId')?.getValue() ?? -1, 344 ); 345 return this.getLayerDisplayName(windowId); 346 }) 347 .join(', '), 348 }, 349 ], 350 eventTree, 351 dispatchTree, 352 sfEntry, 353 ); 354 } 355 356 private getLayerDisplayName(layerId: number): string { 357 // Surround the name using the invisible zero-width non-joiner character to ensure 358 // the full string is matched while filtering. 359 return `\u{200C}${ 360 this.layerIdToName.get(layerId) ?? layerId.toString() 361 }\u{200C}`; 362 } 363 364 private static extractKeyDetails( 365 eventTree: PropertyTreeNode, 366 dispatchTree: PropertyTreeNode, 367 ): string { 368 const keyDetails = 369 'Keycode: ' + 370 eventTree 371 .getChildByName('keyCode') 372 ?.formattedValue() 373 ?.replace(/^KEYCODE_/, '') ?? '<?>'; 374 return keyDetails + ' ' + Presenter.extractDispatchDetails(dispatchTree); 375 } 376 377 private static extractDispatchDetails( 378 dispatchTree: PropertyTreeNode, 379 ): string { 380 let details = ''; 381 dispatchTree.getAllChildren().forEach((dispatchEntry) => { 382 const windowIdNode = dispatchEntry.getChildByName('windowId'); 383 if (windowIdNode === undefined) { 384 return; 385 } 386 if (windowIdNode.formattedValue() === '0') { 387 // Skip showing windowId 0, which is an omnipresent system window. 388 return; 389 } 390 details += windowIdNode.getValue() + ', '; 391 }); 392 return '[' + details.slice(0, -2) + ']'; 393 } 394 395 protected override async updatePropertiesTree() { 396 await super.updatePropertiesTree(); 397 await this.updateDispatchPropertiesTree(); 398 await this.updateRects(); 399 } 400 401 private async updateDispatchPropertiesTree() { 402 const inputEntry = this.getCurrentEntry(); 403 const tree = inputEntry?.dispatchPropertiesTree; 404 this.dispatchPropertiesPresenter.setPropertiesTree(tree); 405 await this.dispatchPropertiesPresenter.formatPropertiesTree( 406 undefined, 407 undefined, 408 this.keepCalculated ?? false, 409 this.trace.type, 410 ); 411 this.uiData.dispatchPropertiesTree = 412 this.dispatchPropertiesPresenter.getFormattedTree(); 413 } 414 415 private async updateRects() { 416 if (this.surfaceFlingerTrace === undefined) { 417 return; 418 } 419 const inputEntry = this.getCurrentEntry(); 420 421 this.currentTargetWindowIds.clear(); 422 inputEntry?.dispatchPropertiesTree 423 ?.getAllChildren() 424 ?.forEach((dispatchEntry) => { 425 const windowId = dispatchEntry.getChildByName('windowId'); 426 if (windowId !== undefined) { 427 this.currentTargetWindowIds.add(`${Number(windowId.getValue())}`); 428 } 429 }); 430 431 if (inputEntry?.surfaceFlingerEntry !== undefined) { 432 const startTimeMs = Date.now(); 433 const node = await inputEntry.surfaceFlingerEntry.getValue(); 434 this.rectsPresenter.applyHierarchyTreesChange([ 435 {trace: this.surfaceFlingerTrace, trees: [node]}, 436 ]); 437 Analytics.Navigation.logFetchComponentDataTime( 438 'rects', 439 TRACE_INFO[TraceType.INPUT_EVENT_MERGED].name, 440 false, 441 Date.now() - startTimeMs, 442 ); 443 444 this.uiData.rectsToDraw = this.rectsPresenter.getRectsToDraw(); 445 this.uiData.rectIdToShowState = 446 this.rectsPresenter.getRectIdToShowState(); 447 } else { 448 this.uiData.rectsToDraw = []; 449 this.uiData.rectIdToShowState = undefined; 450 } 451 this.uiData.rectsUserOptions = this.rectsPresenter.getUserOptions(); 452 this.uiData.displays = this.rectsPresenter.getDisplays(); 453 } 454 455 private getCurrentEntry(): InputEntry | undefined { 456 const entries = this.logPresenter.getFilteredEntries(); 457 const selectedIndex = this.logPresenter.getSelectedIndex(); 458 const currentIndex = this.logPresenter.getCurrentIndex(); 459 const index = selectedIndex ?? currentIndex; 460 if (index === undefined) { 461 return undefined; 462 } 463 return entries[index]; 464 } 465 466 protected override addViewerSpecificListeners(htmlElement: HTMLElement) { 467 htmlElement.addEventListener( 468 ViewerEvents.HighlightedPropertyChange, 469 (event) => 470 this.onHighlightedPropertyChange((event as CustomEvent).detail.id), 471 ); 472 473 htmlElement.addEventListener(ViewerEvents.HighlightedIdChange, (event) => 474 this.onHighlightedIdChange((event as CustomEvent).detail.id), 475 ); 476 477 htmlElement.addEventListener( 478 ViewerEvents.RectsUserOptionsChange, 479 async (event) => { 480 await this.onRectsUserOptionsChange( 481 (event as CustomEvent).detail.userOptions, 482 ); 483 }, 484 ); 485 486 htmlElement.addEventListener(ViewerEvents.RectsDblClick, async (event) => { 487 await this.onRectDoubleClick(); 488 }); 489 490 htmlElement.addEventListener( 491 ViewerEvents.DispatchPropertiesFilterChange, 492 async (event) => { 493 const detail: TextFilter = (event as CustomEvent).detail; 494 await this.onDispatchPropertiesFilterChange(detail); 495 }, 496 ); 497 } 498 499 onHighlightedPropertyChange(id: string) { 500 this.propertiesPresenter.applyHighlightedPropertyChange(id); 501 this.dispatchPropertiesPresenter.applyHighlightedPropertyChange(id); 502 this.uiData.highlightedProperty = 503 id === this.uiData.highlightedProperty ? '' : id; 504 this.notifyViewChanged(); 505 } 506 507 async onHighlightedIdChange(id: string) { 508 this.uiData.highlightedRect = id === this.uiData.highlightedRect ? '' : id; 509 await this.updateRects(); 510 this.notifyViewChanged(); 511 } 512 513 async onRectsUserOptionsChange(userOptions: UserOptions) { 514 this.rectsPresenter.applyRectsUserOptionsChange(userOptions); 515 await this.updateRects(); 516 this.notifyViewChanged(); 517 } 518 519 async onRectDoubleClick() { 520 await this.emitAppEvent( 521 new TabbedViewSwitchRequest(assertDefined(this.surfaceFlingerTrace)), 522 ); 523 } 524} 525