Files
app_Settings/src/com/android/settings/search/SearchFragment.java
Soroosh Mariooryad 683ccdf97b Move static search ranking from DatabaseResultLoader to Search Adapter.
This will avoid unnecessary static ranking if smart ranking is used.

Since loader does not need to provided sorted collection of results,
the loading data type has changed from List<> to Set<>. This will also
faster lookup in the Adapter.

Fixes: 38447799
Test: make RunSettingsRoboTests

Change-Id: I448b29bd4e8700c8ec4b5766cbeab4b3087ae39a
2017-05-26 09:28:06 -07:00

378 lines
13 KiB
Java

/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.android.settings.search;
import android.app.ActionBar;
import android.app.Activity;
import android.app.LoaderManager;
import android.content.Context;
import android.content.Loader;
import android.os.Bundle;
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import android.widget.SearchView;
import com.android.internal.logging.nano.MetricsProto;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.core.InstrumentedFragment;
import com.android.settings.core.instrumentation.MetricsFeatureProvider;
import com.android.settings.overlay.FeatureFactory;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
/**
* This fragment manages the lifecycle of indexing and searching.
*
* In onCreate, the indexing process is initiated in DatabaseIndexingManager.
* While the indexing is happening, loaders are blocked from accessing the database, but the user
* is free to start typing their query.
*
* When the indexing is complete, the fragment gets a callback to initialize the loaders and search
* the query if the user has entered text.
*/
public class SearchFragment extends InstrumentedFragment implements SearchView.OnQueryTextListener,
LoaderManager.LoaderCallbacks<Set<? extends SearchResult>>, IndexingCallback {
private static final String TAG = "SearchFragment";
@VisibleForTesting
static final int SEARCH_TAG = "SearchViewTag".hashCode();
// State values
private static final String STATE_QUERY = "state_query";
private static final String STATE_SHOWING_SAVED_QUERY = "state_showing_saved_query";
private static final String STATE_NEVER_ENTERED_QUERY = "state_never_entered_query";
private static final String STATE_RESULT_CLICK_COUNT = "state_result_click_count";
// Loader IDs
@VisibleForTesting
static final int LOADER_ID_DATABASE = 1;
@VisibleForTesting
static final int LOADER_ID_INSTALLED_APPS = 2;
private static final int NUM_QUERY_LOADERS = 2;
@VisibleForTesting
AtomicInteger mUnfinishedLoadersCount = new AtomicInteger(NUM_QUERY_LOADERS);
// Logging
@VisibleForTesting
static final String RESULT_CLICK_COUNT = "settings_search_result_click_count";
@VisibleForTesting
String mQuery;
private boolean mNeverEnteredQuery = true;
@VisibleForTesting
boolean mShowingSavedQuery;
private int mResultClickCount;
private MetricsFeatureProvider mMetricsFeatureProvider;
@VisibleForTesting
SavedQueryController mSavedQueryController;
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
SearchFeatureProvider mSearchFeatureProvider;
private SearchResultsAdapter mSearchAdapter;
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
RecyclerView mResultsRecyclerView;
@VisibleForTesting
SearchView mSearchView;
private LinearLayout mNoResultsView;
@VisibleForTesting
final RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dy != 0) {
hideKeyboard();
}
}
};
@Override
public int getMetricsCategory() {
return MetricsProto.MetricsEvent.DASHBOARD_SEARCH_RESULTS;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
mSearchFeatureProvider = FeatureFactory.getFactory(context).getSearchFeatureProvider();
mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
final LoaderManager loaderManager = getLoaderManager();
mSearchAdapter = new SearchResultsAdapter(this, mSearchFeatureProvider);
mSavedQueryController = new SavedQueryController(
getContext(), loaderManager, mSearchAdapter);
mSearchFeatureProvider.initFeedbackButton();
if (savedInstanceState != null) {
mQuery = savedInstanceState.getString(STATE_QUERY);
mNeverEnteredQuery = savedInstanceState.getBoolean(STATE_NEVER_ENTERED_QUERY);
mResultClickCount = savedInstanceState.getInt(STATE_RESULT_CLICK_COUNT);
mShowingSavedQuery = savedInstanceState.getBoolean(STATE_SHOWING_SAVED_QUERY);
} else {
mShowingSavedQuery = true;
}
final Activity activity = getActivity();
final ActionBar actionBar = activity.getActionBar();
mSearchView = makeSearchView(actionBar, mQuery);
actionBar.setCustomView(mSearchView);
actionBar.setDisplayShowCustomEnabled(true);
actionBar.setDisplayShowTitleEnabled(false);
mSearchView.requestFocus();
// Run the Index update only if we have some space
if (!Utils.isLowStorage(activity)) {
mSearchFeatureProvider.updateIndex(activity, this /* indexingCallback */);
} else {
Log.w(TAG, "Cannot update the Indexer as we are running low on storage space!");
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.search_panel, container, false);
mResultsRecyclerView = view.findViewById(R.id.list_results);
mResultsRecyclerView.setAdapter(mSearchAdapter);
mResultsRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
mResultsRecyclerView.addOnScrollListener(mScrollListener);
mNoResultsView = view.findViewById(R.id.no_results_layout);
return view;
}
@Override
public void onResume() {
super.onResume();
requery();
}
@Override
public void onStop() {
super.onStop();
final Activity activity = getActivity();
if (activity != null && activity.isFinishing()) {
mMetricsFeatureProvider.histogram(activity, RESULT_CLICK_COUNT, mResultClickCount);
if (mNeverEnteredQuery) {
mMetricsFeatureProvider.action(activity,
MetricsProto.MetricsEvent.ACTION_LEAVE_SEARCH_RESULT_WITHOUT_QUERY);
}
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(STATE_QUERY, mQuery);
outState.putBoolean(STATE_NEVER_ENTERED_QUERY, mNeverEnteredQuery);
outState.putBoolean(STATE_SHOWING_SAVED_QUERY, mShowingSavedQuery);
outState.putInt(STATE_RESULT_CLICK_COUNT, mResultClickCount);
}
@Override
public boolean onQueryTextChange(String query) {
if (TextUtils.equals(query, mQuery)) {
return true;
}
final boolean isEmptyQuery = TextUtils.isEmpty(query);
// Hide no-results-view when the new query is not a super-string of the previous
if ((mQuery != null) && (mNoResultsView.getVisibility() == View.VISIBLE)
&& (query.length() < mQuery.length())) {
mNoResultsView.setVisibility(View.GONE);
}
mResultClickCount = 0;
mNeverEnteredQuery = false;
mQuery = query;
// If indexing is not finished, register the query text, but don't search.
if (!mSearchFeatureProvider.isIndexingComplete(getActivity())) {
return true;
}
if (isEmptyQuery) {
final LoaderManager loaderManager = getLoaderManager();
loaderManager.destroyLoader(LOADER_ID_DATABASE);
loaderManager.destroyLoader(LOADER_ID_INSTALLED_APPS);
mShowingSavedQuery = true;
mSavedQueryController.loadSavedQueries();
mSearchFeatureProvider.hideFeedbackButton();
} else {
restartLoaders();
}
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
// Save submitted query.
mSavedQueryController.saveQuery(mQuery);
hideKeyboard();
return true;
}
@Override
public Loader<Set<? extends SearchResult>> onCreateLoader(int id, Bundle args) {
final Activity activity = getActivity();
switch (id) {
case LOADER_ID_DATABASE:
return mSearchFeatureProvider.getDatabaseSearchLoader(activity, mQuery);
case LOADER_ID_INSTALLED_APPS:
return mSearchFeatureProvider.getInstalledAppSearchLoader(activity, mQuery);
default:
return null;
}
}
@Override
public void onLoadFinished(Loader<Set<? extends SearchResult>> loader,
Set<? extends SearchResult> data) {
mSearchAdapter.addSearchResults(data, loader.getClass().getName());
if (mUnfinishedLoadersCount.decrementAndGet() != 0) {
return;
}
final int resultCount = mSearchAdapter.displaySearchResults(mQuery);
if (resultCount == 0) {
mNoResultsView.setVisibility(View.VISIBLE);
} else {
mNoResultsView.setVisibility(View.GONE);
mResultsRecyclerView.scrollToPosition(0);
}
mSearchFeatureProvider.showFeedbackButton(this, getView());
}
@Override
public void onLoaderReset(Loader<Set<? extends SearchResult>> loader) {
}
/**
* Gets called when Indexing is completed.
*/
@Override
public void onIndexingFinished() {
if (getActivity() == null) {
return;
}
if (mShowingSavedQuery) {
mSavedQueryController.loadSavedQueries();
} else {
final LoaderManager loaderManager = getLoaderManager();
loaderManager.initLoader(LOADER_ID_DATABASE, null, this);
loaderManager.initLoader(LOADER_ID_INSTALLED_APPS, null, this);
}
requery();
}
public void onSearchResultClicked() {
mSavedQueryController.saveQuery(mQuery);
mResultClickCount++;
}
public void onSavedQueryClicked(CharSequence query) {
final String queryString = query.toString();
mMetricsFeatureProvider.action(getContext(),
MetricsProto.MetricsEvent.ACTION_CLICK_SETTINGS_SEARCH_SAVED_QUERY);
mSearchView.setQuery(queryString, false /* submit */);
onQueryTextChange(queryString);
}
public void onRemoveSavedQueryClicked(CharSequence title) {
mSavedQueryController.removeQuery(title.toString());
}
private void restartLoaders() {
mShowingSavedQuery = false;
final LoaderManager loaderManager = getLoaderManager();
mUnfinishedLoadersCount.set(NUM_QUERY_LOADERS);
loaderManager.restartLoader(LOADER_ID_DATABASE, null /* args */, this /* callback */);
loaderManager.restartLoader(LOADER_ID_INSTALLED_APPS, null /* args */, this /* callback */);
}
public String getQuery() {
return mQuery;
}
public List<SearchResult> getSearchResults() {
return mSearchAdapter.getSearchResults();
}
private void requery() {
if (TextUtils.isEmpty(mQuery)) {
return;
}
final String query = mQuery;
mQuery = "";
onQueryTextChange(query);
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
SearchView makeSearchView(ActionBar actionBar, String query) {
final SearchView searchView = new SearchView(actionBar.getThemedContext());
searchView.setIconifiedByDefault(false);
searchView.setQuery(query, false /* submitQuery */);
searchView.setOnQueryTextListener(this);
searchView.setTag(SEARCH_TAG, searchView);
final LayoutParams lp =
new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
searchView.setLayoutParams(lp);
return searchView;
}
private void hideKeyboard() {
final Activity activity = getActivity();
if (activity != null) {
View view = activity.getCurrentFocus();
InputMethodManager imm = (InputMethodManager)
activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
if (mResultsRecyclerView != null) {
mResultsRecyclerView.requestFocus();
}
}
}