• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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 package com.android.settings.search2;
18 
19 import android.app.ActionBar;
20 import android.app.Activity;
21 import android.app.LoaderManager;
22 import android.content.Context;
23 import android.content.Loader;
24 import android.os.Bundle;
25 import android.support.annotation.VisibleForTesting;
26 import android.support.v7.widget.LinearLayoutManager;
27 import android.support.v7.widget.RecyclerView;
28 import android.text.TextUtils;
29 import android.util.Log;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.view.inputmethod.InputMethodManager;
34 import android.widget.LinearLayout;
35 import android.widget.LinearLayout.LayoutParams;
36 import android.widget.SearchView;
37 
38 import com.android.internal.logging.nano.MetricsProto;
39 import com.android.settings.R;
40 import com.android.settings.Utils;
41 import com.android.settings.core.InstrumentedFragment;
42 import com.android.settings.core.instrumentation.MetricsFeatureProvider;
43 import com.android.settings.overlay.FeatureFactory;
44 import com.android.settings.search.IndexingCallback;
45 
46 import java.util.List;
47 import java.util.concurrent.atomic.AtomicInteger;
48 
49 /**
50  * This fragment manages the lifecycle of indexing and searching.
51  *
52  * In onCreate, the indexing process is initiated in DatabaseIndexingManager.
53  * While the indexing is happening, loaders are blocked from accessing the database, but the user
54  * is free to start typing their query.
55  *
56  * When the indexing is complete, the fragment gets a callback to initialize the loaders and search
57  * the query if the user has entered text.
58  */
59 public class SearchFragment extends InstrumentedFragment implements SearchView.OnQueryTextListener,
60         LoaderManager.LoaderCallbacks<List<? extends SearchResult>>, IndexingCallback {
61     private static final String TAG = "SearchFragment";
62 
63     @VisibleForTesting
64     static final int SEARCH_TAG = "SearchViewTag".hashCode();
65 
66     // State values
67     private static final String STATE_QUERY = "state_query";
68     private static final String STATE_SHOWING_SAVED_QUERY = "state_showing_saved_query";
69     private static final String STATE_NEVER_ENTERED_QUERY = "state_never_entered_query";
70     private static final String STATE_RESULT_CLICK_COUNT = "state_result_click_count";
71 
72     // Loader IDs
73     @VisibleForTesting
74     static final int LOADER_ID_DATABASE = 1;
75     @VisibleForTesting
76     static final int LOADER_ID_INSTALLED_APPS = 2;
77 
78     private static final int NUM_QUERY_LOADERS = 2;
79 
80     @VisibleForTesting
81     AtomicInteger mUnfinishedLoadersCount = new AtomicInteger(NUM_QUERY_LOADERS);
82 
83     // Logging
84     @VisibleForTesting
85     static final String RESULT_CLICK_COUNT = "settings_search_result_click_count";
86 
87     @VisibleForTesting
88     String mQuery;
89 
90     private boolean mNeverEnteredQuery = true;
91     @VisibleForTesting
92     boolean mShowingSavedQuery;
93     private int mResultClickCount;
94     private MetricsFeatureProvider mMetricsFeatureProvider;
95     @VisibleForTesting
96     SavedQueryController mSavedQueryController;
97 
98     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
99     SearchFeatureProvider mSearchFeatureProvider;
100 
101     private SearchResultsAdapter mSearchAdapter;
102 
103     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
104     RecyclerView mResultsRecyclerView;
105     @VisibleForTesting
106     SearchView mSearchView;
107     private LinearLayout mNoResultsView;
108 
109     @VisibleForTesting
110     final RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
111         @Override
112         public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
113             if (dy != 0) {
114                 hideKeyboard();
115             }
116         }
117     };
118 
119     @Override
getMetricsCategory()120     public int getMetricsCategory() {
121         return MetricsProto.MetricsEvent.DASHBOARD_SEARCH_RESULTS;
122     }
123 
124     @Override
onAttach(Context context)125     public void onAttach(Context context) {
126         super.onAttach(context);
127         mSearchFeatureProvider = FeatureFactory.getFactory(context).getSearchFeatureProvider();
128         mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
129     }
130 
131     @Override
onCreate(Bundle savedInstanceState)132     public void onCreate(Bundle savedInstanceState) {
133         super.onCreate(savedInstanceState);
134         setHasOptionsMenu(true);
135 
136         final LoaderManager loaderManager = getLoaderManager();
137         mSearchAdapter = new SearchResultsAdapter(this);
138         mSavedQueryController = new SavedQueryController(
139                 getContext(), loaderManager, mSearchAdapter);
140         mSearchFeatureProvider.initFeedbackButton();
141 
142         if (savedInstanceState != null) {
143             mQuery = savedInstanceState.getString(STATE_QUERY);
144             mNeverEnteredQuery = savedInstanceState.getBoolean(STATE_NEVER_ENTERED_QUERY);
145             mResultClickCount = savedInstanceState.getInt(STATE_RESULT_CLICK_COUNT);
146             mShowingSavedQuery = savedInstanceState.getBoolean(STATE_SHOWING_SAVED_QUERY);
147         } else {
148             mShowingSavedQuery = true;
149         }
150 
151         final Activity activity = getActivity();
152         final ActionBar actionBar = activity.getActionBar();
153         mSearchView = makeSearchView(actionBar, mQuery);
154         actionBar.setCustomView(mSearchView);
155         actionBar.setDisplayShowCustomEnabled(true);
156         actionBar.setDisplayShowTitleEnabled(false);
157         mSearchView.requestFocus();
158 
159         // Run the Index update only if we have some space
160         if (!Utils.isLowStorage(activity)) {
161             mSearchFeatureProvider.updateIndex(activity, this /* indexingCallback */);
162         } else {
163             Log.w(TAG, "Cannot update the Indexer as we are running low on storage space!");
164         }
165     }
166 
167     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)168     public View onCreateView(LayoutInflater inflater, ViewGroup container,
169             Bundle savedInstanceState) {
170         final View view = inflater.inflate(R.layout.search_panel, container, false);
171         mResultsRecyclerView = view.findViewById(R.id.list_results);
172         mResultsRecyclerView.setAdapter(mSearchAdapter);
173         mResultsRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
174         mResultsRecyclerView.addOnScrollListener(mScrollListener);
175 
176         mNoResultsView = view.findViewById(R.id.no_results_layout);
177         return view;
178     }
179 
180     @Override
onResume()181     public void onResume() {
182         super.onResume();
183         requery();
184     }
185 
186     @Override
onStop()187     public void onStop() {
188         super.onStop();
189         final Activity activity = getActivity();
190         if (activity != null && activity.isFinishing()) {
191             mMetricsFeatureProvider.histogram(activity, RESULT_CLICK_COUNT, mResultClickCount);
192             if (mNeverEnteredQuery) {
193                 mMetricsFeatureProvider.action(activity,
194                         MetricsProto.MetricsEvent.ACTION_LEAVE_SEARCH_RESULT_WITHOUT_QUERY);
195             }
196         }
197     }
198 
199     @Override
onSaveInstanceState(Bundle outState)200     public void onSaveInstanceState(Bundle outState) {
201         super.onSaveInstanceState(outState);
202         outState.putString(STATE_QUERY, mQuery);
203         outState.putBoolean(STATE_NEVER_ENTERED_QUERY, mNeverEnteredQuery);
204         outState.putBoolean(STATE_SHOWING_SAVED_QUERY, mShowingSavedQuery);
205         outState.putInt(STATE_RESULT_CLICK_COUNT, mResultClickCount);
206     }
207 
208     @Override
onQueryTextChange(String query)209     public boolean onQueryTextChange(String query) {
210         if (TextUtils.equals(query, mQuery)) {
211             return true;
212         }
213 
214         final boolean isEmptyQuery = TextUtils.isEmpty(query);
215 
216         // Hide no-results-view when the new query is not a super-string of the previous
217         if ((mQuery != null) && (mNoResultsView.getVisibility() == View.VISIBLE)
218                 && (query.length() < mQuery.length())) {
219             mNoResultsView.setVisibility(View.GONE);
220         }
221 
222         mResultClickCount = 0;
223         mNeverEnteredQuery = false;
224         mQuery = query;
225 
226         // If indexing is not finished, register the query text, but don't search.
227         if (!mSearchFeatureProvider.isIndexingComplete(getActivity())) {
228             return true;
229         }
230 
231         if (isEmptyQuery) {
232             final LoaderManager loaderManager = getLoaderManager();
233             loaderManager.destroyLoader(LOADER_ID_DATABASE);
234             loaderManager.destroyLoader(LOADER_ID_INSTALLED_APPS);
235             mShowingSavedQuery = true;
236             mSavedQueryController.loadSavedQueries();
237             mSearchFeatureProvider.hideFeedbackButton();
238         } else {
239             restartLoaders();
240         }
241 
242         return true;
243     }
244 
245     @Override
onQueryTextSubmit(String query)246     public boolean onQueryTextSubmit(String query) {
247         // Save submitted query.
248         mSavedQueryController.saveQuery(mQuery);
249         hideKeyboard();
250         return true;
251     }
252 
253     @Override
onCreateLoader(int id, Bundle args)254     public Loader<List<? extends SearchResult>> onCreateLoader(int id, Bundle args) {
255         final Activity activity = getActivity();
256 
257         switch (id) {
258             case LOADER_ID_DATABASE:
259                 return mSearchFeatureProvider.getDatabaseSearchLoader(activity, mQuery);
260             case LOADER_ID_INSTALLED_APPS:
261                 return mSearchFeatureProvider.getInstalledAppSearchLoader(activity, mQuery);
262             default:
263                 return null;
264         }
265     }
266 
267     @Override
onLoadFinished(Loader<List<? extends SearchResult>> loader, List<? extends SearchResult> data)268     public void onLoadFinished(Loader<List<? extends SearchResult>> loader,
269             List<? extends SearchResult> data) {
270         mSearchAdapter.addSearchResults(data, loader.getClass().getName());
271         if (mUnfinishedLoadersCount.decrementAndGet() != 0) {
272             return;
273         }
274         final int resultCount = mSearchAdapter.displaySearchResults();
275 
276         if (resultCount == 0) {
277             mNoResultsView.setVisibility(View.VISIBLE);
278         } else {
279             mNoResultsView.setVisibility(View.GONE);
280             mResultsRecyclerView.scrollToPosition(0);
281         }
282         mSearchFeatureProvider.showFeedbackButton(this, getView());
283     }
284 
285     @Override
onLoaderReset(Loader<List<? extends SearchResult>> loader)286     public void onLoaderReset(Loader<List<? extends SearchResult>> loader) {
287     }
288 
289     /**
290      * Gets called when Indexing is completed.
291      */
292     @Override
onIndexingFinished()293     public void onIndexingFinished() {
294         if (getActivity() == null) {
295             return;
296         }
297         if (mShowingSavedQuery) {
298             mSavedQueryController.loadSavedQueries();
299         } else {
300             final LoaderManager loaderManager = getLoaderManager();
301             loaderManager.initLoader(LOADER_ID_DATABASE, null, this);
302             loaderManager.initLoader(LOADER_ID_INSTALLED_APPS, null, this);
303         }
304 
305         requery();
306     }
307 
onSearchResultClicked()308     public void onSearchResultClicked() {
309         mSavedQueryController.saveQuery(mQuery);
310         mResultClickCount++;
311     }
312 
onSavedQueryClicked(CharSequence query)313     public void onSavedQueryClicked(CharSequence query) {
314         final String queryString = query.toString();
315         mMetricsFeatureProvider.action(getContext(),
316                 MetricsProto.MetricsEvent.ACTION_CLICK_SETTINGS_SEARCH_SAVED_QUERY);
317         mSearchView.setQuery(queryString, false /* submit */);
318         onQueryTextChange(queryString);
319     }
320 
onRemoveSavedQueryClicked(CharSequence title)321     public void onRemoveSavedQueryClicked(CharSequence title) {
322         mSavedQueryController.removeQuery(title.toString());
323     }
324 
restartLoaders()325     private void restartLoaders() {
326         mShowingSavedQuery = false;
327         final LoaderManager loaderManager = getLoaderManager();
328         mUnfinishedLoadersCount.set(NUM_QUERY_LOADERS);
329         loaderManager.restartLoader(LOADER_ID_DATABASE, null /* args */, this /* callback */);
330         loaderManager.restartLoader(LOADER_ID_INSTALLED_APPS, null /* args */, this /* callback */);
331     }
332 
getQuery()333     public String getQuery() {
334         return mQuery;
335     }
336 
getSearchResults()337     public List<SearchResult> getSearchResults() {
338         return mSearchAdapter.getSearchResults();
339     }
340 
requery()341     private void requery() {
342         if (TextUtils.isEmpty(mQuery)) {
343             return;
344         }
345         final String query = mQuery;
346         mQuery = "";
347         onQueryTextChange(query);
348     }
349 
350     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
makeSearchView(ActionBar actionBar, String query)351     SearchView makeSearchView(ActionBar actionBar, String query) {
352         final SearchView searchView = new SearchView(actionBar.getThemedContext());
353         searchView.setIconifiedByDefault(false);
354         searchView.setQuery(query, false /* submitQuery */);
355         searchView.setOnQueryTextListener(this);
356         searchView.setTag(SEARCH_TAG, searchView);
357         final LayoutParams lp =
358                 new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
359         searchView.setLayoutParams(lp);
360         return searchView;
361     }
362 
hideKeyboard()363     private void hideKeyboard() {
364         final Activity activity = getActivity();
365         if (activity != null) {
366             View view = activity.getCurrentFocus();
367             InputMethodManager imm = (InputMethodManager)
368                     activity.getSystemService(Context.INPUT_METHOD_SERVICE);
369             imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
370         }
371 
372         if (mResultsRecyclerView != null) {
373             mResultsRecyclerView.requestFocus();
374         }
375     }
376 }