1 /*
2  * Copyright (C) 2017 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  */
17 
18 package com.android.settings.intelligence.search;
19 
20 import static com.android.settings.intelligence.nano.SettingsIntelligenceLogProto.SettingsIntelligenceEvent;
21 
22 import android.app.Activity;
23 import android.app.Fragment;
24 import android.app.LoaderManager;
25 import android.content.Context;
26 import android.content.Loader;
27 import android.os.Bundle;
28 import androidx.annotation.VisibleForTesting;
29 import androidx.recyclerview.widget.LinearLayoutManager;
30 import androidx.recyclerview.widget.RecyclerView;
31 import android.text.TextUtils;
32 import android.util.EventLog;
33 import android.util.Log;
34 import android.view.LayoutInflater;
35 import android.view.Menu;
36 import android.view.MenuInflater;
37 import android.view.View;
38 import android.view.ViewGroup;
39 import android.view.inputmethod.InputMethodManager;
40 import android.widget.LinearLayout;
41 import android.widget.SearchView;
42 import android.widget.Toolbar;
43 
44 import com.android.settings.intelligence.R;
45 import com.android.settings.intelligence.instrumentation.MetricsFeatureProvider;
46 import com.android.settings.intelligence.overlay.FeatureFactory;
47 import com.android.settings.intelligence.search.indexing.IndexingCallback;
48 import com.android.settings.intelligence.search.savedqueries.SavedQueryController;
49 import com.android.settings.intelligence.search.savedqueries.SavedQueryViewHolder;
50 
51 import java.util.List;
52 
53 /**
54  * This fragment manages the lifecycle of indexing and searching.
55  *
56  * In onCreate, the indexing process is initiated in DatabaseIndexingManager.
57  * While the indexing is happening, loaders are blocked from accessing the database, but the user
58  * is free to start typing their query.
59  *
60  * When the indexing is complete, the fragment gets a callback to initialize the loaders and search
61  * the query if the user has entered text.
62  */
63 public class SearchFragment extends Fragment implements SearchView.OnQueryTextListener,
64         LoaderManager.LoaderCallbacks<List<? extends SearchResult>>, IndexingCallback {
65     private static final String TAG = "SearchFragment";
66 
67     // State values
68     private static final String STATE_QUERY = "state_query";
69     private static final String STATE_SHOWING_SAVED_QUERY = "state_showing_saved_query";
70     private static final String STATE_NEVER_ENTERED_QUERY = "state_never_entered_query";
71 
72     public static final class SearchLoaderId {
73         // Search Query IDs
74         public static final int SEARCH_RESULT = 1;
75 
76         // Saved Query IDs
77         public static final int SAVE_QUERY_TASK = 2;
78         public static final int REMOVE_QUERY_TASK = 3;
79         public static final int SAVED_QUERIES = 4;
80     }
81 
82     @VisibleForTesting
83     String mQuery;
84 
85     private boolean mNeverEnteredQuery = true;
86     private long mEnterQueryTimestampMs;
87 
88     @VisibleForTesting
89     boolean mShowingSavedQuery;
90     private MetricsFeatureProvider mMetricsFeatureProvider;
91     @VisibleForTesting
92     SavedQueryController mSavedQueryController;
93 
94     @VisibleForTesting
95     SearchFeatureProvider mSearchFeatureProvider;
96 
97     @VisibleForTesting
98     SearchResultsAdapter mSearchAdapter;
99 
100     @VisibleForTesting
101     RecyclerView mResultsRecyclerView;
102     @VisibleForTesting
103     SearchView mSearchView;
104     @VisibleForTesting
105     LinearLayout mNoResultsView;
106 
107     @VisibleForTesting
108     final RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
109         @Override
110         public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
111             if (dy != 0) {
112                 hideKeyboard();
113             }
114         }
115     };
116 
117     @Override
onAttach(Context context)118     public void onAttach(Context context) {
119         super.onAttach(context);
120         mSearchFeatureProvider = FeatureFactory.get(context).searchFeatureProvider();
121         mMetricsFeatureProvider = FeatureFactory.get(context).metricsFeatureProvider(context);
122     }
123 
124     @Override
onCreate(Bundle savedInstanceState)125     public void onCreate(Bundle savedInstanceState) {
126         super.onCreate(savedInstanceState);
127         long startTime = System.currentTimeMillis();
128         setHasOptionsMenu(true);
129 
130         final LoaderManager loaderManager = getLoaderManager();
131         mSearchAdapter = new SearchResultsAdapter(this /* fragment */);
132         mSavedQueryController = new SavedQueryController(
133                 getContext(), loaderManager, mSearchAdapter);
134         mSearchFeatureProvider.initFeedbackButton();
135 
136         if (savedInstanceState != null) {
137             mQuery = savedInstanceState.getString(STATE_QUERY);
138             mNeverEnteredQuery = savedInstanceState.getBoolean(STATE_NEVER_ENTERED_QUERY);
139             mShowingSavedQuery = savedInstanceState.getBoolean(STATE_SHOWING_SAVED_QUERY);
140         } else {
141             mShowingSavedQuery = true;
142         }
143         mSearchFeatureProvider.updateIndexAsync(getContext(), this /* indexingCallback */);
144         if (SearchFeatureProvider.DEBUG) {
145             Log.d(TAG, "onCreate spent " + (System.currentTimeMillis() - startTime) + " ms");
146         }
147     }
148 
149     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)150     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
151         super.onCreateOptionsMenu(menu, inflater);
152         mSavedQueryController.buildMenuItem(menu);
153     }
154 
155     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)156     public View onCreateView(LayoutInflater inflater, ViewGroup container,
157             Bundle savedInstanceState) {
158         final Activity activity = getActivity();
159         final View view = inflater.inflate(R.layout.search_panel, container, false);
160         mResultsRecyclerView = view.findViewById(R.id.list_results);
161         mResultsRecyclerView.setAdapter(mSearchAdapter);
162         mResultsRecyclerView.setLayoutManager(new LinearLayoutManager(activity));
163         mResultsRecyclerView.addOnScrollListener(mScrollListener);
164 
165         mNoResultsView = view.findViewById(R.id.no_results_layout);
166 
167         final Toolbar toolbar = view.findViewById(R.id.search_toolbar);
168         activity.setActionBar(toolbar);
169         activity.getActionBar().setDisplayHomeAsUpEnabled(true);
170 
171         mSearchView = toolbar.findViewById(R.id.search_view);
172         mSearchView.setQuery(mQuery, false /* submitQuery */);
173         mSearchView.setOnQueryTextListener(this);
174         mSearchView.requestFocus();
175 
176         return view;
177     }
178 
179     @Override
onStart()180     public void onStart() {
181         super.onStart();
182         mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.OPEN_SEARCH_PAGE);
183     }
184 
185     @Override
onResume()186     public void onResume() {
187         super.onResume();
188         Context appContext = getContext().getApplicationContext();
189         if (mSearchFeatureProvider.isSmartSearchRankingEnabled(appContext)) {
190             mSearchFeatureProvider.searchRankingWarmup(appContext);
191         }
192         requery();
193     }
194 
195     @Override
onStop()196     public void onStop() {
197         super.onStop();
198         mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.LEAVE_SEARCH_PAGE);
199         final Activity activity = getActivity();
200         if (activity != null && activity.isFinishing()) {
201             if (mNeverEnteredQuery) {
202                 mMetricsFeatureProvider.logEvent(
203                         SettingsIntelligenceEvent.LEAVE_SEARCH_WITHOUT_QUERY);
204             }
205         }
206     }
207 
208     @Override
onSaveInstanceState(Bundle outState)209     public void onSaveInstanceState(Bundle outState) {
210         super.onSaveInstanceState(outState);
211         outState.putString(STATE_QUERY, mQuery);
212         outState.putBoolean(STATE_NEVER_ENTERED_QUERY, mNeverEnteredQuery);
213         outState.putBoolean(STATE_SHOWING_SAVED_QUERY, mShowingSavedQuery);
214     }
215 
216     @Override
onQueryTextChange(String query)217     public boolean onQueryTextChange(String query) {
218         if (TextUtils.equals(query, mQuery)) {
219             return true;
220         }
221         mEnterQueryTimestampMs = System.currentTimeMillis();
222         final boolean isEmptyQuery = TextUtils.isEmpty(query);
223 
224         // Hide no-results-view when the new query is not a super-string of the previous
225         if (mQuery != null
226                 && mNoResultsView.getVisibility() == View.VISIBLE
227                 && query.length() < mQuery.length()) {
228             mNoResultsView.setVisibility(View.GONE);
229         }
230 
231         mNeverEnteredQuery = false;
232         mQuery = query;
233 
234         // If indexing is not finished, register the query text, but don't search.
235         if (!mSearchFeatureProvider.isIndexingComplete(getActivity())) {
236             return true;
237         }
238 
239         if (isEmptyQuery) {
240             final LoaderManager loaderManager = getLoaderManager();
241             loaderManager.destroyLoader(SearchLoaderId.SEARCH_RESULT);
242             mShowingSavedQuery = true;
243             mSavedQueryController.loadSavedQueries();
244             mSearchFeatureProvider.hideFeedbackButton(getView());
245         } else {
246             mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.PERFORM_SEARCH);
247             restartLoaders();
248         }
249 
250         return true;
251     }
252 
253     @Override
onQueryTextSubmit(String query)254     public boolean onQueryTextSubmit(String query) {
255         // Save submitted query.
256         mSavedQueryController.saveQuery(mQuery);
257         hideKeyboard();
258         return true;
259     }
260 
261     @Override
onCreateLoader(int id, Bundle args)262     public Loader<List<? extends SearchResult>> onCreateLoader(int id, Bundle args) {
263         final Activity activity = getActivity();
264 
265         switch (id) {
266             case SearchLoaderId.SEARCH_RESULT:
267                 return mSearchFeatureProvider.getSearchResultLoader(activity, mQuery);
268             default:
269                 return null;
270         }
271     }
272 
273     @Override
onLoadFinished(Loader<List<? extends SearchResult>> loader, List<? extends SearchResult> data)274     public void onLoadFinished(Loader<List<? extends SearchResult>> loader,
275             List<? extends SearchResult> data) {
276         mSearchAdapter.postSearchResults(data);
277     }
278 
279     @Override
onLoaderReset(Loader<List<? extends SearchResult>> loader)280     public void onLoaderReset(Loader<List<? extends SearchResult>> loader) {
281     }
282 
283     /**
284      * Gets called when Indexing is completed.
285      */
286     @Override
onIndexingFinished()287     public void onIndexingFinished() {
288         if (getActivity() == null) {
289             return;
290         }
291         if (mShowingSavedQuery) {
292             mSavedQueryController.loadSavedQueries();
293         } else {
294             final LoaderManager loaderManager = getLoaderManager();
295             loaderManager.initLoader(SearchLoaderId.SEARCH_RESULT, null /* args */,
296                     this /* callback */);
297         }
298 
299         requery();
300     }
301 
getSearchResults()302     public List<SearchResult> getSearchResults() {
303         return mSearchAdapter.getSearchResults();
304     }
305 
onSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result)306     public void onSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result) {
307         logSearchResultClicked(resultViewHolder, result);
308         mSearchFeatureProvider.searchResultClicked(getContext(), mQuery, result);
309         mSavedQueryController.saveQuery(mQuery);
310     }
311 
onSearchResultsDisplayed(int resultCount)312     public void onSearchResultsDisplayed(int resultCount) {
313         final long queryToResultLatencyMs = mEnterQueryTimestampMs > 0
314                 ? System.currentTimeMillis() - mEnterQueryTimestampMs
315                 : 0;
316         if (resultCount == 0) {
317             mNoResultsView.setVisibility(View.VISIBLE);
318             mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.SHOW_SEARCH_NO_RESULT,
319                     queryToResultLatencyMs);
320             EventLog.writeEvent(90204 /* settings_latency*/, 1 /* query_to_result_latency */,
321                     (int) queryToResultLatencyMs);
322         } else {
323             mNoResultsView.setVisibility(View.GONE);
324             mResultsRecyclerView.scrollToPosition(0);
325             mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.SHOW_SEARCH_RESULT,
326                     queryToResultLatencyMs);
327         }
328         mSearchFeatureProvider.showFeedbackButton(this, getView());
329     }
330 
onSavedQueryClicked(SavedQueryViewHolder vh, CharSequence query)331     public void onSavedQueryClicked(SavedQueryViewHolder vh, CharSequence query) {
332         final String queryString = query.toString();
333         mMetricsFeatureProvider.logEvent(vh.getClickActionMetricName());
334         mSearchView.setQuery(queryString, false /* submit */);
335         onQueryTextChange(queryString);
336     }
337 
restartLoaders()338     private void restartLoaders() {
339         mShowingSavedQuery = false;
340         final LoaderManager loaderManager = getLoaderManager();
341         loaderManager.restartLoader(
342                 SearchLoaderId.SEARCH_RESULT, null /* args */, this /* callback */);
343     }
344 
getQuery()345     public String getQuery() {
346         return mQuery;
347     }
348 
requery()349     private void requery() {
350         if (TextUtils.isEmpty(mQuery)) {
351             return;
352         }
353         final String query = mQuery;
354         mQuery = "";
355         onQueryTextChange(query);
356     }
357 
hideKeyboard()358     private void hideKeyboard() {
359         final Activity activity = getActivity();
360         if (activity != null) {
361             View view = activity.getCurrentFocus();
362             if (view != null) {
363                 InputMethodManager imm = (InputMethodManager)
364                         activity.getSystemService(Context.INPUT_METHOD_SERVICE);
365                 imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
366             }
367         }
368 
369         if (mResultsRecyclerView != null) {
370             mResultsRecyclerView.requestFocus();
371         }
372     }
373 
logSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result)374     private void logSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result) {
375         final int resultType = resultViewHolder.getClickActionMetricName();
376         final int resultCount = mSearchAdapter.getItemCount();
377         final int resultRank = resultViewHolder.getAdapterPosition();
378         mMetricsFeatureProvider.logSearchResultClick(result, mQuery, resultType, resultCount,
379                 resultRank);
380     }
381 }
382