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 }