xref: /aosp_15_r20/development/tools/winscope/src/viewers/viewer_search/presenter.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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