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 {FunctionUtils} from 'common/function_utils'; 19import {PersistentStoreProxy} from 'common/store/persistent_store_proxy'; 20import {Store} from 'common/store/store'; 21import {TimestampConverter} from 'common/time/timestamp_converter'; 22import { 23 InitializeTraceSearchRequest, 24 TracePositionUpdate, 25 TraceRemoveRequest, 26 TraceSearchRequest, 27 WinscopeEvent, 28 WinscopeEventType, 29} from 'messaging/winscope_event'; 30import {EmitEvent} from 'messaging/winscope_event_emitter'; 31import {Trace} from 'trace/trace'; 32import {Traces} from 'trace/traces'; 33import {TraceType} from 'trace/trace_type'; 34import {QueryResult} from 'trace_processor/query_result'; 35import { 36 AddQueryClickDetail, 37 ClearQueryClickDetail, 38 DeleteSavedQueryClickDetail, 39 SaveQueryClickDetail, 40 SearchQueryClickDetail, 41 ViewerEvents, 42} from 'viewers/common/viewer_events'; 43import {SearchResultPresenter} from './search_result_presenter'; 44import {CurrentSearch, ListedSearch, SearchResult, UiData} from './ui_data'; 45 46interface ActiveSearch { 47 search: CurrentSearch; 48 trace?: Trace<QueryResult>; 49 resultPresenter?: SearchResultPresenter; 50} 51 52export class Presenter { 53 private emitWinscopeEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC; 54 private uiData = UiData.createEmpty(); 55 private activeSearchUid = 0; 56 private activeSearches: ActiveSearch[] = []; 57 private savedSearches = PersistentStoreProxy.new<{searches: ListedSearch[]}>( 58 'savedSearches', 59 {searches: []}, 60 this.storage, 61 ); 62 private viewerElement: HTMLElement | undefined; 63 private runningSearch: CurrentSearch | undefined; 64 65 constructor( 66 private traces: Traces, 67 private storage: Store, 68 private readonly notifyViewCallback: (uiData: UiData) => void, 69 private readonly timestampConverter: TimestampConverter, 70 ) { 71 this.uiData.savedSearches = Array.from(this.savedSearches.searches); 72 this.addSearch(); 73 } 74 75 setEmitEvent(callback: EmitEvent) { 76 this.emitWinscopeEvent = callback; 77 } 78 79 addEventListeners(htmlElement: HTMLElement) { 80 this.viewerElement = htmlElement; 81 htmlElement.addEventListener( 82 ViewerEvents.GlobalSearchSectionClick, 83 async (event) => { 84 this.onGlobalSearchSectionClick(); 85 }, 86 ); 87 htmlElement.addEventListener( 88 ViewerEvents.SearchQueryClick, 89 async (event) => { 90 const detail: SearchQueryClickDetail = (event as CustomEvent).detail; 91 this.onSearchQueryClick(detail.query, detail.uid); 92 }, 93 ); 94 htmlElement.addEventListener(ViewerEvents.SaveQueryClick, async (event) => { 95 const detail: SaveQueryClickDetail = (event as CustomEvent).detail; 96 this.onSaveQueryClick(detail.query, detail.name); 97 }); 98 htmlElement.addEventListener( 99 ViewerEvents.DeleteSavedQueryClick, 100 async (event) => { 101 const detail: DeleteSavedQueryClickDetail = (event as CustomEvent) 102 .detail; 103 this.onDeleteSavedQueryClick(detail.search); 104 }, 105 ); 106 htmlElement.addEventListener(ViewerEvents.AddQueryClick, async (event) => { 107 const detail: AddQueryClickDetail | undefined = (event as CustomEvent) 108 .detail; 109 this.addSearch(detail?.query); 110 }); 111 htmlElement.addEventListener( 112 ViewerEvents.ClearQueryClick, 113 async (event) => { 114 const detail: ClearQueryClickDetail = (event as CustomEvent).detail; 115 this.onClearQueryClick(detail.uid); 116 }, 117 ); 118 } 119 120 async onAppEvent(event: WinscopeEvent) { 121 await event.visit( 122 WinscopeEventType.TRACE_SEARCH_INITIALIZED, 123 async (event) => { 124 this.uiData.searchViews = event.views; 125 this.uiData.initialized = true; 126 this.copyUiDataAndNotifyView(); 127 }, 128 ); 129 await event.visit(WinscopeEventType.TRACE_ADD_REQUEST, async (event) => { 130 if (event.trace.type === TraceType.SEARCH) { 131 this.showQueryResult(event.trace as Trace<QueryResult>); 132 } 133 }); 134 await event.visit(WinscopeEventType.TRACE_SEARCH_FAILED, async (event) => { 135 this.onTraceSearchFailed(); 136 }); 137 for (const activeSearch of this.activeSearches.values()) { 138 await activeSearch.resultPresenter?.onAppEvent(event); 139 } 140 } 141 142 async onGlobalSearchSectionClick() { 143 if (!this.uiData.initialized) { 144 this.emitWinscopeEvent(new InitializeTraceSearchRequest()); 145 } 146 } 147 148 async onSearchQueryClick(query: string, uid: number) { 149 const activeSearch = assertDefined( 150 this.activeSearches.find((a) => a.search.uid === uid), 151 ); 152 this.resetActiveSearch(activeSearch, query); 153 this.runningSearch = activeSearch.search; 154 this.emitWinscopeEvent(new TraceSearchRequest(query)); 155 } 156 157 addSearch(query?: string) { 158 this.activeSearchUid++; 159 this.activeSearches.push({ 160 search: new CurrentSearch(this.activeSearchUid, query), 161 }); 162 this.updateCurrentSearches(); 163 } 164 165 async onClearQueryClick(uid: number) { 166 const activeSearchIndex = this.activeSearches.findIndex( 167 (a) => a.search.uid === uid, 168 ); 169 if (activeSearchIndex === -1) { 170 return; 171 } 172 const activeSearch = this.activeSearches.splice(activeSearchIndex, 1)[0]; 173 this.resetActiveSearch(activeSearch); 174 this.updateCurrentSearches(); 175 } 176 177 onSaveQueryClick(query: string, name: string) { 178 this.uiData.savedSearches.unshift(new ListedSearch(query, name)); 179 this.savedSearches.searches = this.uiData.savedSearches; 180 this.copyUiDataAndNotifyView(); 181 } 182 183 onDeleteSavedQueryClick(savedSearch: ListedSearch) { 184 this.uiData.savedSearches = this.uiData.savedSearches.filter( 185 (s) => s !== savedSearch, 186 ); 187 this.savedSearches.searches = this.uiData.savedSearches; 188 this.copyUiDataAndNotifyView(); 189 } 190 191 private onTraceSearchFailed() { 192 this.runningSearch = undefined; 193 this.uiData.lastTraceFailed = true; 194 this.copyUiDataAndNotifyView(); 195 this.uiData.lastTraceFailed = false; 196 } 197 198 private async showQueryResult(newTrace: Trace<QueryResult>) { 199 const [traceQuery] = newTrace.getDescriptors(); 200 if (this.uiData.recentSearches.length >= 10) { 201 this.uiData.recentSearches.pop(); 202 } 203 this.uiData.recentSearches.unshift(new ListedSearch(traceQuery)); 204 205 const activeSearch = assertDefined( 206 this.activeSearches.find((a) => a.search.uid === this.runningSearch?.uid), 207 ); 208 this.resetActiveSearch(activeSearch, traceQuery); 209 this.initializeResultPresenter(activeSearch, newTrace); 210 this.runningSearch = undefined; 211 this.copyUiDataAndNotifyView(); 212 } 213 214 private updateCurrentSearches() { 215 this.uiData.currentSearches = this.activeSearches.map((a) => a.search); 216 this.copyUiDataAndNotifyView(); 217 } 218 219 private resetActiveSearch(activeSearch: ActiveSearch, newQuery?: string) { 220 activeSearch.search.query = newQuery; 221 activeSearch.search.result = undefined; 222 if (activeSearch.resultPresenter) { 223 activeSearch.resultPresenter.onDestroy(); 224 activeSearch.resultPresenter = undefined; 225 } 226 if (activeSearch.trace) { 227 this.emitWinscopeEvent(new TraceRemoveRequest(activeSearch.trace)); 228 activeSearch.trace = undefined; 229 } 230 } 231 232 private async initializeResultPresenter( 233 activeSearch: ActiveSearch, 234 newTrace: Trace<QueryResult>, 235 ) { 236 activeSearch.trace = newTrace; 237 const firstEntry = 238 newTrace.lengthEntries > 0 ? newTrace.getEntry(0) : undefined; 239 240 const presenter = new SearchResultPresenter( 241 newTrace, 242 (result: SearchResult) => { 243 if (activeSearch.search.result) { 244 activeSearch.search.result.scrollToIndex = result.scrollToIndex; 245 activeSearch.search.result.selectedIndex = result.selectedIndex; 246 } else { 247 activeSearch.search.result = result; 248 } 249 this.updateCurrentSearches(); 250 }, 251 (valueNs: bigint) => 252 this.timestampConverter.makeTimestampFromBootTimeNs(valueNs), 253 firstEntry ? await firstEntry.getValue() : undefined, 254 ); 255 presenter.addEventListeners(assertDefined(this.viewerElement)); 256 presenter.setEmitEvent(async (event) => this.emitWinscopeEvent(event)); 257 activeSearch.resultPresenter = presenter; 258 259 if (firstEntry) { 260 await this.emitWinscopeEvent( 261 TracePositionUpdate.fromTraceEntry(firstEntry), 262 ); 263 } 264 } 265 266 private copyUiDataAndNotifyView() { 267 // Create a shallow copy of the data, otherwise the Angular OnPush change detection strategy 268 // won't detect the new input 269 const copy = Object.assign({}, this.uiData); 270 this.notifyViewCallback(copy); 271 } 272} 273