blob: e50558bad6eaae2ca8aa43da920a92eef5744be1 [file] [log] [blame]
/*
* 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.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<List<? 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<List<? 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<List<? extends SearchResult>> loader,
List<? 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<List<? 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();
}
}
}