• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.search;
19 
20 import android.app.Activity;
21 import android.app.LoaderManager;
22 import android.content.ComponentName;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.Loader;
26 import android.os.Bundle;
27 import android.support.annotation.VisibleForTesting;
28 import android.support.v7.widget.LinearLayoutManager;
29 import android.support.v7.widget.RecyclerView;
30 import android.text.TextUtils;
31 import android.util.Log;
32 import android.util.Pair;
33 import android.util.TypedValue;
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.TextView;
43 import android.widget.Toolbar;
44 
45 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
46 import com.android.settings.R;
47 import com.android.settings.SettingsActivity;
48 import com.android.settings.Utils;
49 import com.android.settings.core.InstrumentedFragment;
50 import com.android.settings.core.instrumentation.MetricsFeatureProvider;
51 import com.android.settings.overlay.FeatureFactory;
52 import com.android.settings.widget.ActionBarShadowController;
53 
54 import java.util.ArrayList;
55 import java.util.Arrays;
56 import java.util.List;
57 import java.util.Set;
58 import java.util.concurrent.atomic.AtomicInteger;
59 
60 /**
61  * This fragment manages the lifecycle of indexing and searching.
62  *
63  * In onCreate, the indexing process is initiated in DatabaseIndexingManager.
64  * While the indexing is happening, loaders are blocked from accessing the database, but the user
65  * is free to start typing their query.
66  *
67  * When the indexing is complete, the fragment gets a callback to initialize the loaders and search
68  * the query if the user has entered text.
69  */
70 public class SearchFragment extends InstrumentedFragment implements SearchView.OnQueryTextListener,
71         LoaderManager.LoaderCallbacks<Set<? extends SearchResult>>, IndexingCallback {
72     private static final String TAG = "SearchFragment";
73 
74     // State values
75     private static final String STATE_QUERY = "state_query";
76     private static final String STATE_SHOWING_SAVED_QUERY = "state_showing_saved_query";
77     private static final String STATE_NEVER_ENTERED_QUERY = "state_never_entered_query";
78     private static final String STATE_RESULT_CLICK_COUNT = "state_result_click_count";
79 
80     static final class SearchLoaderId {
81         // Search Query IDs
82         public static final int DATABASE = 1;
83         public static final int INSTALLED_APPS = 2;
84         public static final int ACCESSIBILITY_SERVICES = 3;
85         public static final int INPUT_DEVICES = 4;
86 
87         // Saved Query IDs
88         public static final int SAVE_QUERY_TASK = 5;
89         public static final int REMOVE_QUERY_TASK = 6;
90         public static final int SAVED_QUERIES = 7;
91     }
92 
93 
94     private static final int NUM_QUERY_LOADERS = 4;
95 
96     @VisibleForTesting
97     AtomicInteger mUnfinishedLoadersCount = new AtomicInteger(NUM_QUERY_LOADERS);
98 
99     // Logging
100     @VisibleForTesting
101     static final String RESULT_CLICK_COUNT = "settings_search_result_click_count";
102 
103     @VisibleForTesting
104     String mQuery;
105 
106     private boolean mNeverEnteredQuery = true;
107     @VisibleForTesting
108     boolean mShowingSavedQuery;
109     private int mResultClickCount;
110     private MetricsFeatureProvider mMetricsFeatureProvider;
111     @VisibleForTesting
112     SavedQueryController mSavedQueryController;
113 
114     @VisibleForTesting
115     SearchFeatureProvider mSearchFeatureProvider;
116 
117     @VisibleForTesting
118     SearchResultsAdapter mSearchAdapter;
119 
120     @VisibleForTesting
121     RecyclerView mResultsRecyclerView;
122     @VisibleForTesting
123     SearchView mSearchView;
124     @VisibleForTesting
125     LinearLayout mNoResultsView;
126 
127     @VisibleForTesting
128     final RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
129         @Override
130         public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
131             if (dy != 0) {
132                 hideKeyboard();
133             }
134         }
135     };
136 
137     @Override
getMetricsCategory()138     public int getMetricsCategory() {
139         return MetricsEvent.DASHBOARD_SEARCH_RESULTS;
140     }
141 
142     @Override
onAttach(Context context)143     public void onAttach(Context context) {
144         super.onAttach(context);
145         mSearchFeatureProvider = FeatureFactory.getFactory(context).getSearchFeatureProvider();
146         mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
147     }
148 
149     @Override
onCreate(Bundle savedInstanceState)150     public void onCreate(Bundle savedInstanceState) {
151         super.onCreate(savedInstanceState);
152         long startTime = System.currentTimeMillis();
153         setHasOptionsMenu(true);
154 
155         final LoaderManager loaderManager = getLoaderManager();
156         mSearchAdapter = new SearchResultsAdapter(this, mSearchFeatureProvider);
157         mSavedQueryController = new SavedQueryController(
158                 getContext(), loaderManager, mSearchAdapter);
159         mSearchFeatureProvider.initFeedbackButton();
160 
161         if (savedInstanceState != null) {
162             mQuery = savedInstanceState.getString(STATE_QUERY);
163             mNeverEnteredQuery = savedInstanceState.getBoolean(STATE_NEVER_ENTERED_QUERY);
164             mResultClickCount = savedInstanceState.getInt(STATE_RESULT_CLICK_COUNT);
165             mShowingSavedQuery = savedInstanceState.getBoolean(STATE_SHOWING_SAVED_QUERY);
166         } else {
167             mShowingSavedQuery = true;
168         }
169 
170         final Activity activity = getActivity();
171         // Run the Index update only if we have some space
172         if (!Utils.isLowStorage(activity)) {
173             mSearchFeatureProvider.updateIndexAsync(activity, this /* indexingCallback */);
174         } else {
175             Log.w(TAG, "Cannot update the Indexer as we are running low on storage space!");
176         }
177         if (SettingsSearchIndexablesProvider.DEBUG) {
178             Log.d(TAG, "onCreate spent " + (System.currentTimeMillis() - startTime) + " ms");
179         }
180     }
181 
182     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)183     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
184         super.onCreateOptionsMenu(menu, inflater);
185         mSavedQueryController.buildMenuItem(menu);
186     }
187 
188     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)189     public View onCreateView(LayoutInflater inflater, ViewGroup container,
190             Bundle savedInstanceState) {
191         final View view = inflater.inflate(R.layout.search_panel, container, false);
192         mResultsRecyclerView = view.findViewById(R.id.list_results);
193         mResultsRecyclerView.setAdapter(mSearchAdapter);
194         mResultsRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
195         mResultsRecyclerView.addOnScrollListener(mScrollListener);
196 
197         mNoResultsView = view.findViewById(R.id.no_results_layout);
198 
199         Toolbar toolbar = view.findViewById(R.id.search_toolbar);
200         getActivity().setActionBar(toolbar);
201         getActivity().getActionBar().setDisplayHomeAsUpEnabled(true);
202 
203         mSearchView = toolbar.findViewById(R.id.search_view);
204         mSearchView.setQuery(mQuery, false /* submitQuery */);
205         mSearchView.setOnQueryTextListener(this);
206         mSearchView.requestFocus();
207 
208         // Updating internal views inside SearchView was the easiest way to get this too look right.
209         // Instead of grabbing the TextView directly, we grab it as a view and do an instanceof
210         // check. This ensures if we return, say, a LinearLayout in the tests, they won't fail.
211         View searchText = mSearchView.findViewById(com.android.internal.R.id.search_src_text);
212         if (searchText instanceof TextView) {
213             TextView searchTextView = (TextView) searchText;
214             searchTextView.setTextColor(getContext().getColorStateList(
215                     com.android.internal.R.color.text_color_primary));
216             searchTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
217                     getResources().getDimension(R.dimen.search_bar_text_size));
218 
219         }
220         View editFrame = mSearchView.findViewById(com.android.internal.R.id.search_edit_frame);
221         if (editFrame != null) {
222             ViewGroup.MarginLayoutParams params =
223                     (ViewGroup.MarginLayoutParams) editFrame.getLayoutParams();
224             params.setMarginStart(0);
225             editFrame.setLayoutParams(params);
226         }
227         ActionBarShadowController.attachToRecyclerView(
228                 view.findViewById(R.id.search_bar_container), getLifecycle(), mResultsRecyclerView);
229         return view;
230     }
231 
232     @Override
onResume()233     public void onResume() {
234         super.onResume();
235         Context appContext = getContext().getApplicationContext();
236         if (mSearchFeatureProvider.isSmartSearchRankingEnabled(appContext)) {
237             mSearchFeatureProvider.searchRankingWarmup(appContext);
238         }
239         requery();
240     }
241 
242     @Override
onStop()243     public void onStop() {
244         super.onStop();
245         final Activity activity = getActivity();
246         if (activity != null && activity.isFinishing()) {
247             mMetricsFeatureProvider.histogram(activity, RESULT_CLICK_COUNT, mResultClickCount);
248             if (mNeverEnteredQuery) {
249                 mMetricsFeatureProvider.action(activity,
250                         MetricsEvent.ACTION_LEAVE_SEARCH_RESULT_WITHOUT_QUERY);
251             }
252         }
253     }
254 
255     @Override
onSaveInstanceState(Bundle outState)256     public void onSaveInstanceState(Bundle outState) {
257         super.onSaveInstanceState(outState);
258         outState.putString(STATE_QUERY, mQuery);
259         outState.putBoolean(STATE_NEVER_ENTERED_QUERY, mNeverEnteredQuery);
260         outState.putBoolean(STATE_SHOWING_SAVED_QUERY, mShowingSavedQuery);
261         outState.putInt(STATE_RESULT_CLICK_COUNT, mResultClickCount);
262     }
263 
264     @Override
onQueryTextChange(String query)265     public boolean onQueryTextChange(String query) {
266         if (TextUtils.equals(query, mQuery)) {
267             return true;
268         }
269 
270         final boolean isEmptyQuery = TextUtils.isEmpty(query);
271 
272         // Hide no-results-view when the new query is not a super-string of the previous
273         if (mQuery != null
274                 && mNoResultsView.getVisibility() == View.VISIBLE
275                 && query.length() < mQuery.length()) {
276             mNoResultsView.setVisibility(View.GONE);
277         }
278 
279         mResultClickCount = 0;
280         mNeverEnteredQuery = false;
281         mQuery = query;
282 
283         // If indexing is not finished, register the query text, but don't search.
284         if (!mSearchFeatureProvider.isIndexingComplete(getActivity())) {
285             return true;
286         }
287 
288         if (isEmptyQuery) {
289             final LoaderManager loaderManager = getLoaderManager();
290             loaderManager.destroyLoader(SearchLoaderId.DATABASE);
291             loaderManager.destroyLoader(SearchLoaderId.INSTALLED_APPS);
292             loaderManager.destroyLoader(SearchLoaderId.ACCESSIBILITY_SERVICES);
293             loaderManager.destroyLoader(SearchLoaderId.INPUT_DEVICES);
294             mShowingSavedQuery = true;
295             mSavedQueryController.loadSavedQueries();
296             mSearchFeatureProvider.hideFeedbackButton();
297         } else {
298             mSearchAdapter.initializeSearch(mQuery);
299             restartLoaders();
300         }
301 
302         return true;
303     }
304 
305     @Override
onQueryTextSubmit(String query)306     public boolean onQueryTextSubmit(String query) {
307         // Save submitted query.
308         mSavedQueryController.saveQuery(mQuery);
309         hideKeyboard();
310         return true;
311     }
312 
313     @Override
onCreateLoader(int id, Bundle args)314     public Loader<Set<? extends SearchResult>> onCreateLoader(int id, Bundle args) {
315         final Activity activity = getActivity();
316 
317         switch (id) {
318             case SearchLoaderId.DATABASE:
319                 return mSearchFeatureProvider.getDatabaseSearchLoader(activity, mQuery);
320             case SearchLoaderId.INSTALLED_APPS:
321                 return mSearchFeatureProvider.getInstalledAppSearchLoader(activity, mQuery);
322             case SearchLoaderId.ACCESSIBILITY_SERVICES:
323                 return mSearchFeatureProvider.getAccessibilityServiceResultLoader(activity, mQuery);
324             case SearchLoaderId.INPUT_DEVICES:
325                 return mSearchFeatureProvider.getInputDeviceResultLoader(activity, mQuery);
326             default:
327                 return null;
328         }
329     }
330 
331     @Override
onLoadFinished(Loader<Set<? extends SearchResult>> loader, Set<? extends SearchResult> data)332     public void onLoadFinished(Loader<Set<? extends SearchResult>> loader,
333             Set<? extends SearchResult> data) {
334         mSearchAdapter.addSearchResults(data, loader.getClass().getName());
335         if (mUnfinishedLoadersCount.decrementAndGet() != 0) {
336             return;
337         }
338 
339         mSearchAdapter.notifyResultsLoaded();
340     }
341 
342     @Override
onLoaderReset(Loader<Set<? extends SearchResult>> loader)343     public void onLoaderReset(Loader<Set<? extends SearchResult>> loader) {
344     }
345 
346     /**
347      * Gets called when Indexing is completed.
348      */
349     @Override
onIndexingFinished()350     public void onIndexingFinished() {
351         if (getActivity() == null) {
352             return;
353         }
354         if (mShowingSavedQuery) {
355             mSavedQueryController.loadSavedQueries();
356         } else {
357             final LoaderManager loaderManager = getLoaderManager();
358             loaderManager.initLoader(SearchLoaderId.DATABASE, null /* args */, this /* callback */);
359             loaderManager.initLoader(
360                     SearchLoaderId.INSTALLED_APPS, null /* args */, this /* callback */);
361             loaderManager.initLoader(
362                     SearchLoaderId.ACCESSIBILITY_SERVICES, null /* args */, this /* callback */);
363             loaderManager.initLoader(
364                     SearchLoaderId.INPUT_DEVICES, null /* args */, this /* callback */);
365         }
366 
367         requery();
368     }
369 
onSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result, Pair<Integer, Object>... logTaggedData)370     public void onSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result,
371             Pair<Integer, Object>... logTaggedData) {
372         logSearchResultClicked(resultViewHolder, result, logTaggedData);
373         mSearchFeatureProvider.searchResultClicked(getContext(), mQuery, result);
374         mSavedQueryController.saveQuery(mQuery);
375         mResultClickCount++;
376     }
377 
onSearchResultsDisplayed(int resultCount)378     public void onSearchResultsDisplayed(int resultCount) {
379         if (resultCount == 0) {
380             mNoResultsView.setVisibility(View.VISIBLE);
381             mMetricsFeatureProvider.visible(getContext(), getMetricsCategory(),
382                     MetricsEvent.SETTINGS_SEARCH_NO_RESULT);
383         } else {
384             mNoResultsView.setVisibility(View.GONE);
385             mResultsRecyclerView.scrollToPosition(0);
386         }
387         mSearchFeatureProvider.showFeedbackButton(this, getView());
388     }
389 
onSavedQueryClicked(CharSequence query)390     public void onSavedQueryClicked(CharSequence query) {
391         final String queryString = query.toString();
392         mMetricsFeatureProvider.action(getContext(),
393                 MetricsEvent.ACTION_CLICK_SETTINGS_SEARCH_SAVED_QUERY);
394         mSearchView.setQuery(queryString, false /* submit */);
395         onQueryTextChange(queryString);
396     }
397 
restartLoaders()398     private void restartLoaders() {
399         mShowingSavedQuery = false;
400         final LoaderManager loaderManager = getLoaderManager();
401         mUnfinishedLoadersCount.set(NUM_QUERY_LOADERS);
402         loaderManager.restartLoader(
403                 SearchLoaderId.DATABASE, null /* args */, this /* callback */);
404         loaderManager.restartLoader(
405                 SearchLoaderId.INSTALLED_APPS, null /* args */, this /* callback */);
406         loaderManager.restartLoader(
407                 SearchLoaderId.ACCESSIBILITY_SERVICES, null /* args */, this /* callback */);
408         loaderManager.restartLoader(
409                 SearchLoaderId.INPUT_DEVICES, null /* args */, this /* callback */);
410     }
411 
getQuery()412     public String getQuery() {
413         return mQuery;
414     }
415 
getSearchResults()416     public List<SearchResult> getSearchResults() {
417         return mSearchAdapter.getSearchResults();
418     }
419 
requery()420     private void requery() {
421         if (TextUtils.isEmpty(mQuery)) {
422             return;
423         }
424         final String query = mQuery;
425         mQuery = "";
426         onQueryTextChange(query);
427     }
428 
hideKeyboard()429     private void hideKeyboard() {
430         final Activity activity = getActivity();
431         if (activity != null) {
432             View view = activity.getCurrentFocus();
433             InputMethodManager imm = (InputMethodManager)
434                     activity.getSystemService(Context.INPUT_METHOD_SERVICE);
435             imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
436         }
437 
438         if (mResultsRecyclerView != null) {
439             mResultsRecyclerView.requestFocus();
440         }
441     }
442 
logSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result, Pair<Integer, Object>... logTaggedData)443     private void logSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result,
444             Pair<Integer, Object>... logTaggedData) {
445         final Intent intent = result.payload.getIntent();
446         if (intent == null) {
447             Log.w(TAG, "Skipped logging click on search result because of null intent, which can " +
448                     "happen on saved query results.");
449             return;
450         }
451         final ComponentName cn = intent.getComponent();
452         String resultName = intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT);
453         if (TextUtils.isEmpty(resultName) && cn != null) {
454             resultName = cn.flattenToString();
455         }
456         final List<Pair<Integer, Object>> taggedData = new ArrayList<>();
457         if (logTaggedData != null) {
458             taggedData.addAll(Arrays.asList(logTaggedData));
459         }
460         taggedData.add(Pair.create(
461                 MetricsEvent.FIELD_SETTINGS_SEARCH_RESULT_COUNT,
462                 mSearchAdapter.getItemCount()));
463         taggedData.add(Pair.create(
464                 MetricsEvent.FIELD_SETTINGS_SEARCH_RESULT_RANK,
465                 resultViewHolder.getAdapterPosition()));
466         taggedData.add(Pair.create(
467                 MetricsEvent.FIELD_SETTINGS_SEARCH_RESULT_ASYNC_RANKING_STATE,
468                 mSearchAdapter.getAsyncRankingState()));
469         taggedData.add(Pair.create(
470                 MetricsEvent.FIELD_SETTINGS_SEARCH_QUERY_LENGTH,
471                 TextUtils.isEmpty(mQuery) ? 0 : mQuery.length()));
472 
473         mMetricsFeatureProvider.action(getContext(),
474                 resultViewHolder.getClickActionMetricName(),
475                 resultName,
476                 taggedData.toArray(new Pair[0]));
477     }
478 }
479