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