Home | History | Annotate | Download | only in search
      1 /*
      2  * Copyright (C) 2017 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 
     18 package com.android.settings.search;
     19 
     20 import android.app.Activity;
     21 import android.app.LoaderManager;
     22 import android.content.ComponentName;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.Loader;
     26 import android.os.Bundle;
     27 import android.support.annotation.VisibleForTesting;
     28 import android.support.v7.widget.LinearLayoutManager;
     29 import android.support.v7.widget.RecyclerView;
     30 import android.text.TextUtils;
     31 import android.util.Log;
     32 import android.util.Pair;
     33 import android.util.TypedValue;
     34 import android.view.LayoutInflater;
     35 import android.view.Menu;
     36 import android.view.MenuInflater;
     37 import android.view.View;
     38 import android.view.ViewGroup;
     39 import android.view.inputmethod.InputMethodManager;
     40 import android.widget.LinearLayout;
     41 import android.widget.SearchView;
     42 import android.widget.TextView;
     43 import android.widget.Toolbar;
     44 
     45 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
     46 import com.android.settings.R;
     47 import com.android.settings.SettingsActivity;
     48 import com.android.settings.Utils;
     49 import com.android.settings.core.InstrumentedFragment;
     50 import com.android.settings.core.instrumentation.MetricsFeatureProvider;
     51 import com.android.settings.overlay.FeatureFactory;
     52 import com.android.settings.widget.ActionBarShadowController;
     53 
     54 import java.util.ArrayList;
     55 import java.util.Arrays;
     56 import java.util.List;
     57 import java.util.Set;
     58 import java.util.concurrent.atomic.AtomicInteger;
     59 
     60 /**
     61  * This fragment manages the lifecycle of indexing and searching.
     62  *
     63  * In onCreate, the indexing process is initiated in DatabaseIndexingManager.
     64  * While the indexing is happening, loaders are blocked from accessing the database, but the user
     65  * is free to start typing their query.
     66  *
     67  * When the indexing is complete, the fragment gets a callback to initialize the loaders and search
     68  * the query if the user has entered text.
     69  */
     70 public class SearchFragment extends InstrumentedFragment implements SearchView.OnQueryTextListener,
     71         LoaderManager.LoaderCallbacks<Set<? extends SearchResult>>, IndexingCallback {
     72     private static final String TAG = "SearchFragment";
     73 
     74     // State values
     75     private static final String STATE_QUERY = "state_query";
     76     private static final String STATE_SHOWING_SAVED_QUERY = "state_showing_saved_query";
     77     private static final String STATE_NEVER_ENTERED_QUERY = "state_never_entered_query";
     78     private static final String STATE_RESULT_CLICK_COUNT = "state_result_click_count";
     79 
     80     static final class SearchLoaderId {
     81         // Search Query IDs
     82         public static final int DATABASE = 1;
     83         public static final int INSTALLED_APPS = 2;
     84         public static final int ACCESSIBILITY_SERVICES = 3;
     85         public static final int INPUT_DEVICES = 4;
     86 
     87         // Saved Query IDs
     88         public static final int SAVE_QUERY_TASK = 5;
     89         public static final int REMOVE_QUERY_TASK = 6;
     90         public static final int SAVED_QUERIES = 7;
     91     }
     92 
     93 
     94     private static final int NUM_QUERY_LOADERS = 4;
     95 
     96     @VisibleForTesting
     97     AtomicInteger mUnfinishedLoadersCount = new AtomicInteger(NUM_QUERY_LOADERS);
     98 
     99     // Logging
    100     @VisibleForTesting
    101     static final String RESULT_CLICK_COUNT = "settings_search_result_click_count";
    102 
    103     @VisibleForTesting
    104     String mQuery;
    105 
    106     private boolean mNeverEnteredQuery = true;
    107     @VisibleForTesting
    108     boolean mShowingSavedQuery;
    109     private int mResultClickCount;
    110     private MetricsFeatureProvider mMetricsFeatureProvider;
    111     @VisibleForTesting
    112     SavedQueryController mSavedQueryController;
    113 
    114     @VisibleForTesting
    115     SearchFeatureProvider mSearchFeatureProvider;
    116 
    117     @VisibleForTesting
    118     SearchResultsAdapter mSearchAdapter;
    119 
    120     @VisibleForTesting
    121     RecyclerView mResultsRecyclerView;
    122     @VisibleForTesting
    123     SearchView mSearchView;
    124     @VisibleForTesting
    125     LinearLayout mNoResultsView;
    126 
    127     @VisibleForTesting
    128     final RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
    129         @Override
    130         public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
    131             if (dy != 0) {
    132                 hideKeyboard();
    133             }
    134         }
    135     };
    136 
    137     @Override
    138     public int getMetricsCategory() {
    139         return MetricsEvent.DASHBOARD_SEARCH_RESULTS;
    140     }
    141 
    142     @Override
    143     public void onAttach(Context context) {
    144         super.onAttach(context);
    145         mSearchFeatureProvider = FeatureFactory.getFactory(context).getSearchFeatureProvider();
    146         mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
    147     }
    148 
    149     @Override
    150     public void onCreate(Bundle savedInstanceState) {
    151         super.onCreate(savedInstanceState);
    152         long startTime = System.currentTimeMillis();
    153         setHasOptionsMenu(true);
    154 
    155         final LoaderManager loaderManager = getLoaderManager();
    156         mSearchAdapter = new SearchResultsAdapter(this, mSearchFeatureProvider);
    157         mSavedQueryController = new SavedQueryController(
    158                 getContext(), loaderManager, mSearchAdapter);
    159         mSearchFeatureProvider.initFeedbackButton();
    160 
    161         if (savedInstanceState != null) {
    162             mQuery = savedInstanceState.getString(STATE_QUERY);
    163             mNeverEnteredQuery = savedInstanceState.getBoolean(STATE_NEVER_ENTERED_QUERY);
    164             mResultClickCount = savedInstanceState.getInt(STATE_RESULT_CLICK_COUNT);
    165             mShowingSavedQuery = savedInstanceState.getBoolean(STATE_SHOWING_SAVED_QUERY);
    166         } else {
    167             mShowingSavedQuery = true;
    168         }
    169 
    170         final Activity activity = getActivity();
    171         // Run the Index update only if we have some space
    172         if (!Utils.isLowStorage(activity)) {
    173             mSearchFeatureProvider.updateIndexAsync(activity, this /* indexingCallback */);
    174         } else {
    175             Log.w(TAG, "Cannot update the Indexer as we are running low on storage space!");
    176         }
    177         if (SettingsSearchIndexablesProvider.DEBUG) {
    178             Log.d(TAG, "onCreate spent " + (System.currentTimeMillis() - startTime) + " ms");
    179         }
    180     }
    181 
    182     @Override
    183     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    184         super.onCreateOptionsMenu(menu, inflater);
    185         mSavedQueryController.buildMenuItem(menu);
    186     }
    187 
    188     @Override
    189     public View onCreateView(LayoutInflater inflater, ViewGroup container,
    190             Bundle savedInstanceState) {
    191         final View view = inflater.inflate(R.layout.search_panel, container, false);
    192         mResultsRecyclerView = view.findViewById(R.id.list_results);
    193         mResultsRecyclerView.setAdapter(mSearchAdapter);
    194         mResultsRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
    195         mResultsRecyclerView.addOnScrollListener(mScrollListener);
    196 
    197         mNoResultsView = view.findViewById(R.id.no_results_layout);
    198 
    199         Toolbar toolbar = view.findViewById(R.id.search_toolbar);
    200         getActivity().setActionBar(toolbar);
    201         getActivity().getActionBar().setDisplayHomeAsUpEnabled(true);
    202 
    203         mSearchView = toolbar.findViewById(R.id.search_view);
    204         mSearchView.setQuery(mQuery, false /* submitQuery */);
    205         mSearchView.setOnQueryTextListener(this);
    206         mSearchView.requestFocus();
    207 
    208         // Updating internal views inside SearchView was the easiest way to get this too look right.
    209         // Instead of grabbing the TextView directly, we grab it as a view and do an instanceof
    210         // check. This ensures if we return, say, a LinearLayout in the tests, they won't fail.
    211         View searchText = mSearchView.findViewById(com.android.internal.R.id.search_src_text);
    212         if (searchText instanceof TextView) {
    213             TextView searchTextView = (TextView) searchText;
    214             searchTextView.setTextColor(getContext().getColorStateList(
    215                     com.android.internal.R.color.text_color_primary));
    216             searchTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
    217                     getResources().getDimension(R.dimen.search_bar_text_size));
    218 
    219         }
    220         View editFrame = mSearchView.findViewById(com.android.internal.R.id.search_edit_frame);
    221         if (editFrame != null) {
    222             ViewGroup.MarginLayoutParams params =
    223                     (ViewGroup.MarginLayoutParams) editFrame.getLayoutParams();
    224             params.setMarginStart(0);
    225             editFrame.setLayoutParams(params);
    226         }
    227         ActionBarShadowController.attachToRecyclerView(
    228                 view.findViewById(R.id.search_bar_container), getLifecycle(), mResultsRecyclerView);
    229         return view;
    230     }
    231 
    232     @Override
    233     public void onResume() {
    234         super.onResume();
    235         Context appContext = getContext().getApplicationContext();
    236         if (mSearchFeatureProvider.isSmartSearchRankingEnabled(appContext)) {
    237             mSearchFeatureProvider.searchRankingWarmup(appContext);
    238         }
    239         requery();
    240     }
    241 
    242     @Override
    243     public void onStop() {
    244         super.onStop();
    245         final Activity activity = getActivity();
    246         if (activity != null && activity.isFinishing()) {
    247             mMetricsFeatureProvider.histogram(activity, RESULT_CLICK_COUNT, mResultClickCount);
    248             if (mNeverEnteredQuery) {
    249                 mMetricsFeatureProvider.action(activity,
    250                         MetricsEvent.ACTION_LEAVE_SEARCH_RESULT_WITHOUT_QUERY);
    251             }
    252         }
    253     }
    254 
    255     @Override
    256     public void onSaveInstanceState(Bundle outState) {
    257         super.onSaveInstanceState(outState);
    258         outState.putString(STATE_QUERY, mQuery);
    259         outState.putBoolean(STATE_NEVER_ENTERED_QUERY, mNeverEnteredQuery);
    260         outState.putBoolean(STATE_SHOWING_SAVED_QUERY, mShowingSavedQuery);
    261         outState.putInt(STATE_RESULT_CLICK_COUNT, mResultClickCount);
    262     }
    263 
    264     @Override
    265     public boolean onQueryTextChange(String query) {
    266         if (TextUtils.equals(query, mQuery)) {
    267             return true;
    268         }
    269 
    270         final boolean isEmptyQuery = TextUtils.isEmpty(query);
    271 
    272         // Hide no-results-view when the new query is not a super-string of the previous
    273         if (mQuery != null
    274                 && mNoResultsView.getVisibility() == View.VISIBLE
    275                 && query.length() < mQuery.length()) {
    276             mNoResultsView.setVisibility(View.GONE);
    277         }
    278 
    279         mResultClickCount = 0;
    280         mNeverEnteredQuery = false;
    281         mQuery = query;
    282 
    283         // If indexing is not finished, register the query text, but don't search.
    284         if (!mSearchFeatureProvider.isIndexingComplete(getActivity())) {
    285             return true;
    286         }
    287 
    288         if (isEmptyQuery) {
    289             final LoaderManager loaderManager = getLoaderManager();
    290             loaderManager.destroyLoader(SearchLoaderId.DATABASE);
    291             loaderManager.destroyLoader(SearchLoaderId.INSTALLED_APPS);
    292             loaderManager.destroyLoader(SearchLoaderId.ACCESSIBILITY_SERVICES);
    293             loaderManager.destroyLoader(SearchLoaderId.INPUT_DEVICES);
    294             mShowingSavedQuery = true;
    295             mSavedQueryController.loadSavedQueries();
    296             mSearchFeatureProvider.hideFeedbackButton();
    297         } else {
    298             mSearchAdapter.initializeSearch(mQuery);
    299             restartLoaders();
    300         }
    301 
    302         return true;
    303     }
    304 
    305     @Override
    306     public boolean onQueryTextSubmit(String query) {
    307         // Save submitted query.
    308         mSavedQueryController.saveQuery(mQuery);
    309         hideKeyboard();
    310         return true;
    311     }
    312 
    313     @Override
    314     public Loader<Set<? extends SearchResult>> onCreateLoader(int id, Bundle args) {
    315         final Activity activity = getActivity();
    316 
    317         switch (id) {
    318             case SearchLoaderId.DATABASE:
    319                 return mSearchFeatureProvider.getDatabaseSearchLoader(activity, mQuery);
    320             case SearchLoaderId.INSTALLED_APPS:
    321                 return mSearchFeatureProvider.getInstalledAppSearchLoader(activity, mQuery);
    322             case SearchLoaderId.ACCESSIBILITY_SERVICES:
    323                 return mSearchFeatureProvider.getAccessibilityServiceResultLoader(activity, mQuery);
    324             case SearchLoaderId.INPUT_DEVICES:
    325                 return mSearchFeatureProvider.getInputDeviceResultLoader(activity, mQuery);
    326             default:
    327                 return null;
    328         }
    329     }
    330 
    331     @Override
    332     public void onLoadFinished(Loader<Set<? extends SearchResult>> loader,
    333             Set<? extends SearchResult> data) {
    334         mSearchAdapter.addSearchResults(data, loader.getClass().getName());
    335         if (mUnfinishedLoadersCount.decrementAndGet() != 0) {
    336             return;
    337         }
    338 
    339         mSearchAdapter.notifyResultsLoaded();
    340     }
    341 
    342     @Override
    343     public void onLoaderReset(Loader<Set<? extends SearchResult>> loader) {
    344     }
    345 
    346     /**
    347      * Gets called when Indexing is completed.
    348      */
    349     @Override
    350     public void onIndexingFinished() {
    351         if (getActivity() == null) {
    352             return;
    353         }
    354         if (mShowingSavedQuery) {
    355             mSavedQueryController.loadSavedQueries();
    356         } else {
    357             final LoaderManager loaderManager = getLoaderManager();
    358             loaderManager.initLoader(SearchLoaderId.DATABASE, null /* args */, this /* callback */);
    359             loaderManager.initLoader(
    360                     SearchLoaderId.INSTALLED_APPS, null /* args */, this /* callback */);
    361             loaderManager.initLoader(
    362                     SearchLoaderId.ACCESSIBILITY_SERVICES, null /* args */, this /* callback */);
    363             loaderManager.initLoader(
    364                     SearchLoaderId.INPUT_DEVICES, null /* args */, this /* callback */);
    365         }
    366 
    367         requery();
    368     }
    369 
    370     public void onSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result,
    371             Pair<Integer, Object>... logTaggedData) {
    372         logSearchResultClicked(resultViewHolder, result, logTaggedData);
    373         mSearchFeatureProvider.searchResultClicked(getContext(), mQuery, result);
    374         mSavedQueryController.saveQuery(mQuery);
    375         mResultClickCount++;
    376     }
    377 
    378     public void onSearchResultsDisplayed(int resultCount) {
    379         if (resultCount == 0) {
    380             mNoResultsView.setVisibility(View.VISIBLE);
    381             mMetricsFeatureProvider.visible(getContext(), getMetricsCategory(),
    382                     MetricsEvent.SETTINGS_SEARCH_NO_RESULT);
    383         } else {
    384             mNoResultsView.setVisibility(View.GONE);
    385             mResultsRecyclerView.scrollToPosition(0);
    386         }
    387         mSearchFeatureProvider.showFeedbackButton(this, getView());
    388     }
    389 
    390     public void onSavedQueryClicked(CharSequence query) {
    391         final String queryString = query.toString();
    392         mMetricsFeatureProvider.action(getContext(),
    393                 MetricsEvent.ACTION_CLICK_SETTINGS_SEARCH_SAVED_QUERY);
    394         mSearchView.setQuery(queryString, false /* submit */);
    395         onQueryTextChange(queryString);
    396     }
    397 
    398     private void restartLoaders() {
    399         mShowingSavedQuery = false;
    400         final LoaderManager loaderManager = getLoaderManager();
    401         mUnfinishedLoadersCount.set(NUM_QUERY_LOADERS);
    402         loaderManager.restartLoader(
    403                 SearchLoaderId.DATABASE, null /* args */, this /* callback */);
    404         loaderManager.restartLoader(
    405                 SearchLoaderId.INSTALLED_APPS, null /* args */, this /* callback */);
    406         loaderManager.restartLoader(
    407                 SearchLoaderId.ACCESSIBILITY_SERVICES, null /* args */, this /* callback */);
    408         loaderManager.restartLoader(
    409                 SearchLoaderId.INPUT_DEVICES, null /* args */, this /* callback */);
    410     }
    411 
    412     public String getQuery() {
    413         return mQuery;
    414     }
    415 
    416     public List<SearchResult> getSearchResults() {
    417         return mSearchAdapter.getSearchResults();
    418     }
    419 
    420     private void requery() {
    421         if (TextUtils.isEmpty(mQuery)) {
    422             return;
    423         }
    424         final String query = mQuery;
    425         mQuery = "";
    426         onQueryTextChange(query);
    427     }
    428 
    429     private void hideKeyboard() {
    430         final Activity activity = getActivity();
    431         if (activity != null) {
    432             View view = activity.getCurrentFocus();
    433             InputMethodManager imm = (InputMethodManager)
    434                     activity.getSystemService(Context.INPUT_METHOD_SERVICE);
    435             imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
    436         }
    437 
    438         if (mResultsRecyclerView != null) {
    439             mResultsRecyclerView.requestFocus();
    440         }
    441     }
    442 
    443     private void logSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result,
    444             Pair<Integer, Object>... logTaggedData) {
    445         final Intent intent = result.payload.getIntent();
    446         if (intent == null) {
    447             Log.w(TAG, "Skipped logging click on search result because of null intent, which can " +
    448                     "happen on saved query results.");
    449             return;
    450         }
    451         final ComponentName cn = intent.getComponent();
    452         String resultName = intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT);
    453         if (TextUtils.isEmpty(resultName) && cn != null) {
    454             resultName = cn.flattenToString();
    455         }
    456         final List<Pair<Integer, Object>> taggedData = new ArrayList<>();
    457         if (logTaggedData != null) {
    458             taggedData.addAll(Arrays.asList(logTaggedData));
    459         }
    460         taggedData.add(Pair.create(
    461                 MetricsEvent.FIELD_SETTINGS_SEARCH_RESULT_COUNT,
    462                 mSearchAdapter.getItemCount()));
    463         taggedData.add(Pair.create(
    464                 MetricsEvent.FIELD_SETTINGS_SEARCH_RESULT_RANK,
    465                 resultViewHolder.getAdapterPosition()));
    466         taggedData.add(Pair.create(
    467                 MetricsEvent.FIELD_SETTINGS_SEARCH_RESULT_ASYNC_RANKING_STATE,
    468                 mSearchAdapter.getAsyncRankingState()));
    469         taggedData.add(Pair.create(
    470                 MetricsEvent.FIELD_SETTINGS_SEARCH_QUERY_LENGTH,
    471                 TextUtils.isEmpty(mQuery) ? 0 : mQuery.length()));
    472 
    473         mMetricsFeatureProvider.action(getContext(),
    474                 resultViewHolder.getClickActionMetricName(),
    475                 resultName,
    476                 taggedData.toArray(new Pair[0]));
    477     }
    478 }
    479