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