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