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