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 {DOMUtils} from 'common/dom_utils'; 18import {FunctionUtils} from 'common/function_utils'; 19import {Timestamp} from 'common/time'; 20import { 21 TracePositionUpdate, 22 WinscopeEvent, 23 WinscopeEventType, 24} from 'messaging/winscope_event'; 25import { 26 EmitEvent, 27 WinscopeEventEmitter, 28} from 'messaging/winscope_event_emitter'; 29import {Trace, TraceEntry} from 'trace/trace'; 30import {TraceEntryFinder} from 'trace/trace_entry_finder'; 31import {TracePosition} from 'trace/trace_position'; 32import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; 33import {PropertiesPresenter} from 'viewers/common/properties_presenter'; 34import {TextFilter} from 'viewers/common/text_filter'; 35import {UserOptions} from 'viewers/common/user_options'; 36import {LogPresenter} from './log_presenter'; 37import {LogEntry, LogHeader, UiDataLog} from './ui_data_log'; 38import { 39 LogFilterChangeDetail, 40 LogTextFilterChangeDetail, 41 TimestampClickDetail, 42 ViewerEvents, 43} from './viewer_events'; 44 45export type NotifyLogViewCallbackType<UiData> = (uiData: UiData) => void; 46 47export abstract class AbstractLogViewerPresenter< 48 UiData extends UiDataLog, 49 TraceEntryType extends object, 50> implements WinscopeEventEmitter 51{ 52 protected static readonly VALUE_NA = 'N/A'; 53 protected emitAppEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC; 54 protected abstract logPresenter: LogPresenter<LogEntry>; 55 protected propertiesPresenter?: PropertiesPresenter; 56 protected keepCalculated?: boolean; 57 private activeTrace?: Trace<object>; 58 private isInitialized = false; 59 60 protected constructor( 61 protected readonly trace: Trace<TraceEntryType>, 62 private readonly notifyViewCallback: NotifyLogViewCallbackType<UiData>, 63 protected readonly uiData: UiData, 64 ) { 65 this.notifyViewChanged(); 66 } 67 68 setEmitEvent(callback: EmitEvent) { 69 this.emitAppEvent = callback; 70 } 71 72 addEventListeners(htmlElement: HTMLElement) { 73 htmlElement.addEventListener( 74 ViewerEvents.LogFilterChange, 75 async (event) => { 76 const detail: LogFilterChangeDetail = (event as CustomEvent).detail; 77 await this.onSelectFilterChange(detail.header, detail.value); 78 }, 79 ); 80 htmlElement.addEventListener( 81 ViewerEvents.LogTextFilterChange, 82 async (event) => { 83 const detail: LogTextFilterChangeDetail = (event as CustomEvent).detail; 84 await this.onTextFilterChange(detail.header, detail.filter); 85 }, 86 ); 87 htmlElement.addEventListener(ViewerEvents.LogEntryClick, async (event) => { 88 await this.onLogEntryClick((event as CustomEvent).detail); 89 }); 90 htmlElement.addEventListener( 91 ViewerEvents.ArrowDownPress, 92 async (event) => await this.onArrowDownPress(), 93 ); 94 htmlElement.addEventListener( 95 ViewerEvents.ArrowUpPress, 96 async (event) => await this.onArrowUpPress(), 97 ); 98 htmlElement.addEventListener(ViewerEvents.TimestampClick, async (event) => { 99 const detail: TimestampClickDetail = (event as CustomEvent).detail; 100 if (detail.entry !== undefined) { 101 await this.onLogTimestampClick(detail.entry); 102 } else if (detail.timestamp !== undefined) { 103 await this.onRawTimestampClick(detail.timestamp); 104 } 105 }); 106 htmlElement.addEventListener( 107 ViewerEvents.PropertiesUserOptionsChange, 108 (event) => 109 this.onPropertiesUserOptionsChange( 110 (event as CustomEvent).detail.userOptions, 111 ), 112 ); 113 htmlElement.addEventListener( 114 ViewerEvents.PropertiesFilterChange, 115 async (event) => { 116 const detail: TextFilter = (event as CustomEvent).detail; 117 await this.onPropertiesFilterChange(detail); 118 }, 119 ); 120 121 document.addEventListener('keydown', async (event: KeyboardEvent) => { 122 const isViewerVisible = DOMUtils.isElementVisible(htmlElement); 123 const isPositionChange = 124 event.key === 'ArrowRight' || event.key === 'ArrowLeft'; 125 if (!isViewerVisible || !isPositionChange) { 126 return; 127 } 128 await this.onPositionChangeByKeyPress(event); 129 }); 130 } 131 132 async onAppEvent(event: WinscopeEvent) { 133 await event.visit( 134 WinscopeEventType.TRACE_POSITION_UPDATE, 135 async (event) => { 136 await this.applyTracePositionUpdate(event); 137 }, 138 ); 139 await event.visit(WinscopeEventType.DARK_MODE_TOGGLED, async (event) => { 140 this.uiData.isDarkMode = event.isDarkMode; 141 this.notifyViewChanged(); 142 }); 143 await event.visit(WinscopeEventType.ACTIVE_TRACE_CHANGED, async (event) => { 144 this.activeTrace = event.trace; 145 }); 146 } 147 148 async onSelectFilterChange(header: LogHeader, value: string[]) { 149 this.logPresenter.applySelectFilterChange(header, value); 150 await this.updatePropertiesTree(); 151 this.uiData.currentIndex = this.logPresenter.getCurrentIndex(); 152 this.uiData.selectedIndex = this.logPresenter.getSelectedIndex(); 153 this.uiData.scrollToIndex = this.logPresenter.getScrollToIndex(); 154 this.uiData.entries = this.logPresenter.getFilteredEntries(); 155 this.notifyViewChanged(); 156 } 157 158 async onTextFilterChange(header: LogHeader, value: TextFilter) { 159 this.logPresenter.applyTextFilterChange(header, value); 160 await this.updatePropertiesTree(); 161 this.uiData.currentIndex = this.logPresenter.getCurrentIndex(); 162 this.uiData.selectedIndex = this.logPresenter.getSelectedIndex(); 163 this.uiData.scrollToIndex = this.logPresenter.getScrollToIndex(); 164 this.uiData.entries = this.logPresenter.getFilteredEntries(); 165 this.notifyViewChanged(); 166 } 167 168 async onPropertiesUserOptionsChange(userOptions: UserOptions) { 169 if (!this.propertiesPresenter) { 170 return; 171 } 172 this.propertiesPresenter.applyPropertiesUserOptionsChange(userOptions); 173 this.uiData.propertiesUserOptions = 174 this.propertiesPresenter.getUserOptions(); 175 await this.updatePropertiesTree(false); 176 this.notifyViewChanged(); 177 } 178 179 async onPropertiesFilterChange(textFilter: TextFilter) { 180 if (!this.propertiesPresenter) { 181 return; 182 } 183 this.propertiesPresenter.applyPropertiesFilterChange(textFilter); 184 await this.updatePropertiesTree(false); 185 this.uiData.propertiesFilter = textFilter; 186 this.notifyViewChanged(); 187 } 188 189 async onLogTimestampClick(traceEntry: TraceEntry<object>) { 190 await this.emitAppEvent( 191 TracePositionUpdate.fromTraceEntry(traceEntry, true), 192 ); 193 } 194 195 async onRawTimestampClick(timestamp: Timestamp) { 196 await this.emitAppEvent(TracePositionUpdate.fromTimestamp(timestamp, true)); 197 } 198 199 async onLogEntryClick(index: number) { 200 this.logPresenter.applyLogEntryClick(index); 201 this.updateIndicesUiData(); 202 await this.updatePropertiesTree(); 203 this.notifyViewChanged(); 204 } 205 206 async onArrowDownPress() { 207 this.logPresenter.applyArrowDownPress(); 208 this.updateIndicesUiData(); 209 await this.updatePropertiesTree(); 210 this.notifyViewChanged(); 211 } 212 213 async onArrowUpPress() { 214 this.logPresenter.applyArrowUpPress(); 215 this.updateIndicesUiData(); 216 await this.updatePropertiesTree(); 217 this.notifyViewChanged(); 218 } 219 220 async onPositionChangeByKeyPress(event: KeyboardEvent) { 221 const currIndex = this.uiData.currentIndex; 222 if (this.activeTrace === this.trace && currIndex !== undefined) { 223 if (event.key === 'ArrowRight') { 224 event.stopImmediatePropagation(); 225 if (currIndex < this.uiData.entries.length - 1) { 226 const currTimestamp = 227 this.uiData.entries[currIndex].traceEntry.getTimestamp(); 228 const nextEntry = this.uiData.entries 229 .slice(currIndex + 1) 230 .find((entry) => entry.traceEntry.getTimestamp() > currTimestamp); 231 if (nextEntry) { 232 return this.emitAppEvent( 233 new TracePositionUpdate( 234 TracePosition.fromTraceEntry(nextEntry.traceEntry), 235 true, 236 ), 237 ); 238 } 239 } 240 } else { 241 event.stopImmediatePropagation(); 242 if (currIndex > 0) { 243 return this.emitAppEvent( 244 new TracePositionUpdate( 245 TracePosition.fromTraceEntry( 246 this.uiData.entries[currIndex - 1].traceEntry, 247 ), 248 true, 249 ), 250 ); 251 } 252 } 253 } 254 } 255 256 protected refreshUiData() { 257 this.uiData.headers = this.logPresenter.getHeaders(); 258 this.uiData.entries = this.logPresenter.getFilteredEntries(); 259 this.uiData.selectedIndex = this.logPresenter.getSelectedIndex(); 260 this.uiData.scrollToIndex = this.logPresenter.getScrollToIndex(); 261 this.uiData.currentIndex = this.logPresenter.getCurrentIndex(); 262 if (this.propertiesPresenter) { 263 this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree(); 264 this.uiData.propertiesUserOptions = 265 this.propertiesPresenter.getUserOptions(); 266 this.uiData.propertiesFilter = this.propertiesPresenter.getTextFilter(); 267 } 268 } 269 270 protected async applyTracePositionUpdate(event: TracePositionUpdate) { 271 await this.initializeIfNeeded(); 272 let entry: TraceEntry<TraceEntryType> | undefined; 273 if (event.position.entry?.getFullTrace() === this.trace) { 274 entry = event.position.entry as TraceEntry<TraceEntryType>; 275 } else { 276 entry = TraceEntryFinder.findCorrespondingEntry( 277 this.trace, 278 event.position, 279 ); 280 } 281 this.logPresenter.applyTracePositionUpdate(entry); 282 283 this.uiData.selectedIndex = this.logPresenter.getSelectedIndex(); 284 this.uiData.scrollToIndex = this.logPresenter.getScrollToIndex(); 285 this.uiData.currentIndex = this.logPresenter.getCurrentIndex(); 286 287 if (this.propertiesPresenter) { 288 await this.updatePropertiesTree(); 289 this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree(); 290 } 291 292 this.notifyViewChanged(); 293 } 294 295 protected async updatePropertiesTree(updateDefaultAllowlist = true) { 296 if (this.propertiesPresenter) { 297 const tree = this.getPropertiesTree(); 298 this.propertiesPresenter.setPropertiesTree(tree); 299 if (updateDefaultAllowlist && this.updateDefaultAllowlist) { 300 this.updateDefaultAllowlist(tree); 301 } 302 await this.propertiesPresenter.formatPropertiesTree( 303 undefined, 304 undefined, 305 this.keepCalculated ?? false, 306 ); 307 this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree(); 308 } 309 } 310 311 private async initializeIfNeeded() { 312 if (this.isInitialized) { 313 return; 314 } 315 316 if (this.initializeTraceSpecificData) { 317 await this.initializeTraceSpecificData(); 318 } 319 320 const headers = this.makeHeaders(); 321 const allEntries = await this.makeUiDataEntries(headers); 322 if (this.updateFiltersInHeaders) { 323 this.updateFiltersInHeaders(headers, allEntries); 324 } 325 326 this.logPresenter.setAllEntries(allEntries); 327 this.logPresenter.setHeaders(headers); 328 this.refreshUiData(); 329 this.isInitialized = true; 330 } 331 332 private updateIndicesUiData() { 333 this.uiData.selectedIndex = this.logPresenter.getSelectedIndex(); 334 this.uiData.currentIndex = this.logPresenter.getCurrentIndex(); 335 this.uiData.scrollToIndex = this.logPresenter.getScrollToIndex(); 336 } 337 338 private getPropertiesTree(): PropertyTreeNode | undefined { 339 const entries = this.logPresenter.getFilteredEntries(); 340 const selectedIndex = this.logPresenter.getSelectedIndex(); 341 const currentIndex = this.logPresenter.getCurrentIndex(); 342 if (selectedIndex !== undefined) { 343 return entries.at(selectedIndex)?.propertiesTree; 344 } 345 if (currentIndex !== undefined) { 346 return entries.at(currentIndex)?.propertiesTree; 347 } 348 return undefined; 349 } 350 351 protected notifyViewChanged() { 352 this.notifyViewCallback(this.uiData); 353 } 354 355 protected abstract makeHeaders(): LogHeader[]; 356 protected abstract makeUiDataEntries( 357 headers: LogHeader[], 358 ): Promise<LogEntry[]>; 359 protected initializeTraceSpecificData?(): Promise<void>; 360 protected updateFiltersInHeaders?( 361 headers: LogHeader[], 362 allEntries: LogEntry[], 363 ): void; 364 protected updateDefaultAllowlist?(tree: PropertyTreeNode | undefined): void; 365} 366