2 * Copyright (C) 2016 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.settings.search2;
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;
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;
45 import java.util.List;
46 import java.util.concurrent.atomic.AtomicInteger;
48 public class SearchFragment extends InstrumentedFragment implements SearchView.OnQueryTextListener,
49 LoaderManager.LoaderCallbacks<List<? extends SearchResult>> {
50 private static final String TAG = "SearchFragment";
53 static final int SEARCH_TAG = "SearchViewTag".hashCode();
56 private static final String STATE_QUERY = "state_query";
57 private static final String STATE_SHOWING_SAVED_QUERY = "state_showing_saved_query";
58 private static final String STATE_NEVER_ENTERED_QUERY = "state_never_entered_query";
59 private static final String STATE_RESULT_CLICK_COUNT = "state_result_click_count";
62 private static final int LOADER_ID_DATABASE = 1;
63 private static final int LOADER_ID_INSTALLED_APPS = 2;
65 private static final int NUM_QUERY_LOADERS = 2;
67 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
68 AtomicInteger mUnfinishedLoadersCount = new AtomicInteger(NUM_QUERY_LOADERS);
71 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
72 static final String RESULT_CLICK_COUNT = "settings_search_result_click_count";
77 private boolean mNeverEnteredQuery = true;
78 private boolean mShowingSavedQuery;
79 private int mResultClickCount;
80 private MetricsFeatureProvider mMetricsFeatureProvider;
81 private SavedQueryController mSavedQueryController;
83 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
84 SearchFeatureProvider mSearchFeatureProvider;
86 private SearchResultsAdapter mSearchAdapter;
88 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
89 RecyclerView mResultsRecyclerView;
90 private SearchView mSearchView;
91 private LinearLayout mNoResultsView;
94 final RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
96 public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
104 public int getMetricsCategory() {
105 return MetricsProto.MetricsEvent.DASHBOARD_SEARCH_RESULTS;
109 public void onAttach(Context context) {
110 super.onAttach(context);
111 mSearchFeatureProvider = FeatureFactory.getFactory(context).getSearchFeatureProvider();
112 mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
116 public void onCreate(Bundle savedInstanceState) {
117 super.onCreate(savedInstanceState);
118 setHasOptionsMenu(true);
119 final LoaderManager loaderManager = getLoaderManager();
120 mSearchAdapter = new SearchResultsAdapter(this);
121 mSavedQueryController = new SavedQueryController(
122 getContext(), loaderManager, mSearchAdapter);
123 mSearchFeatureProvider.initFeedbackButton();
125 if (savedInstanceState != null) {
126 mQuery = savedInstanceState.getString(STATE_QUERY);
127 mNeverEnteredQuery = savedInstanceState.getBoolean(STATE_NEVER_ENTERED_QUERY);
128 mResultClickCount = savedInstanceState.getInt(STATE_RESULT_CLICK_COUNT);
129 mShowingSavedQuery = savedInstanceState.getBoolean(STATE_SHOWING_SAVED_QUERY);
130 if (mShowingSavedQuery) {
131 mSavedQueryController.loadSavedQueries();
133 loaderManager.initLoader(LOADER_ID_DATABASE, null, this);
134 loaderManager.initLoader(LOADER_ID_INSTALLED_APPS, null, this);
137 mShowingSavedQuery = true;
138 mSavedQueryController.loadSavedQueries();
141 final Activity activity = getActivity();
142 final ActionBar actionBar = activity.getActionBar();
143 mSearchView = makeSearchView(actionBar, mQuery);
144 actionBar.setCustomView(mSearchView);
145 actionBar.setDisplayShowCustomEnabled(true);
146 actionBar.setDisplayShowTitleEnabled(false);
147 mSearchView.requestFocus();
149 // Run the Index update only if we have some space
150 if (!Utils.isLowStorage(activity)) {
151 mSearchFeatureProvider.updateIndex(activity);
153 Log.w(TAG, "Cannot update the Indexer as we are running low on storage space!");
158 public View onCreateView(LayoutInflater inflater, ViewGroup container,
159 Bundle savedInstanceState) {
160 final View view = inflater.inflate(R.layout.search_panel_2, container, false);
161 mResultsRecyclerView = view.findViewById(R.id.list_results);
162 mResultsRecyclerView.setAdapter(mSearchAdapter);
163 mResultsRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
164 mResultsRecyclerView.addOnScrollListener(mScrollListener);
166 mNoResultsView = view.findViewById(R.id.no_results_layout);
171 public void onStop() {
173 final Activity activity = getActivity();
174 if (activity != null && activity.isFinishing()) {
175 mMetricsFeatureProvider.histogram(activity, RESULT_CLICK_COUNT, mResultClickCount);
176 if (mNeverEnteredQuery) {
177 mMetricsFeatureProvider.action(activity,
178 MetricsProto.MetricsEvent.ACTION_LEAVE_SEARCH_RESULT_WITHOUT_QUERY);
184 public void onSaveInstanceState(Bundle outState) {
185 super.onSaveInstanceState(outState);
186 outState.putString(STATE_QUERY, mQuery);
187 outState.putBoolean(STATE_NEVER_ENTERED_QUERY, mNeverEnteredQuery);
188 outState.putBoolean(STATE_SHOWING_SAVED_QUERY, mShowingSavedQuery);
189 outState.putInt(STATE_RESULT_CLICK_COUNT, mResultClickCount);
193 public boolean onQueryTextChange(String query) {
194 if (TextUtils.equals(query, mQuery)) {
198 final boolean isEmptyQuery = TextUtils.isEmpty(query);
200 // Hide no-results-view when the new query is not a super-string of the previous
201 if ((mQuery != null) && (mNoResultsView.getVisibility() == View.VISIBLE)
202 && (query.length() < mQuery.length())) {
203 mNoResultsView.setVisibility(View.GONE);
206 mResultClickCount = 0;
207 mNeverEnteredQuery = false;
209 mSearchAdapter.clearResults();
212 final LoaderManager loaderManager = getLoaderManager();
213 loaderManager.destroyLoader(LOADER_ID_DATABASE);
214 loaderManager.destroyLoader(LOADER_ID_INSTALLED_APPS);
215 mShowingSavedQuery = true;
216 mSavedQueryController.loadSavedQueries();
217 mSearchFeatureProvider.hideFeedbackButton();
226 public boolean onQueryTextSubmit(String query) {
227 // Save submitted query.
228 mSavedQueryController.saveQuery(mQuery);
234 public Loader<List<? extends SearchResult>> onCreateLoader(int id, Bundle args) {
235 final Activity activity = getActivity();
238 case LOADER_ID_DATABASE:
239 return mSearchFeatureProvider.getDatabaseSearchLoader(activity, mQuery);
240 case LOADER_ID_INSTALLED_APPS:
241 return mSearchFeatureProvider.getInstalledAppSearchLoader(activity, mQuery);
248 public void onLoadFinished(Loader<List<? extends SearchResult>> loader,
249 List<? extends SearchResult> data) {
250 mSearchAdapter.addSearchResults(data, loader.getClass().getName());
251 if (mUnfinishedLoadersCount.decrementAndGet() != 0) {
254 final int resultCount = mSearchAdapter.displaySearchResults();
255 mNoResultsView.setVisibility(resultCount == 0 ? View.VISIBLE : View.GONE);
256 mSearchFeatureProvider.showFeedbackButton(this, getView());
260 public void onLoaderReset(Loader<List<? extends SearchResult>> loader) {
263 public void onSearchResultClicked() {
264 mSavedQueryController.saveQuery(mQuery);
268 public void onSavedQueryClicked(CharSequence query) {
269 final String queryString = query.toString();
270 mMetricsFeatureProvider.action(getContext(),
271 MetricsProto.MetricsEvent.ACTION_CLICK_SETTINGS_SEARCH_SAVED_QUERY);
272 mSearchView.setQuery(queryString, false /* submit */);
273 onQueryTextChange(queryString);
276 public void onRemoveSavedQueryClicked(CharSequence title) {
277 mSavedQueryController.removeQuery(title.toString());
280 private void restartLoaders() {
281 mShowingSavedQuery = false;
282 final LoaderManager loaderManager = getLoaderManager();
283 mUnfinishedLoadersCount.set(NUM_QUERY_LOADERS);
284 loaderManager.restartLoader(LOADER_ID_DATABASE, null /* args */, this /* callback */);
285 loaderManager.restartLoader(LOADER_ID_INSTALLED_APPS, null /* args */, this /* callback */);
288 public String getQuery() {
292 public List<SearchResult> getSearchResults() {
293 return mSearchAdapter.getSearchResults();
296 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
297 SearchView makeSearchView(ActionBar actionBar, String query) {
298 final SearchView searchView = new SearchView(actionBar.getThemedContext());
299 searchView.setIconifiedByDefault(false);
300 searchView.setQuery(query, false /* submitQuery */);
301 searchView.setOnQueryTextListener(this);
302 searchView.setTag(SEARCH_TAG, searchView);
303 final LayoutParams lp =
304 new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
305 searchView.setLayoutParams(lp);
309 private void hideKeyboard() {
310 final Activity activity = getActivity();
311 if (activity != null) {
312 View view = activity.getCurrentFocus();
313 InputMethodManager imm = (InputMethodManager)
314 activity.getSystemService(Context.INPUT_METHOD_SERVICE);
315 imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
318 if (mResultsRecyclerView != null) {
319 mResultsRecyclerView.requestFocus();