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