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.intelligence.search;
     19 
     20 import static com.android.settings.intelligence.nano.SettingsIntelligenceLogProto.SettingsIntelligenceEvent;
     21 
     22 import android.app.Activity;
     23 import android.app.Fragment;
     24 import android.app.LoaderManager;
     25 import android.content.Context;
     26 import android.content.Loader;
     27 import android.os.Bundle;
     28 import android.support.annotation.VisibleForTesting;
     29 import android.support.v7.widget.LinearLayoutManager;
     30 import android.support.v7.widget.RecyclerView;
     31 import android.text.TextUtils;
     32 import android.util.EventLog;
     33 import android.util.Log;
     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.Toolbar;
     43 
     44 import com.android.settings.intelligence.R;
     45 import com.android.settings.intelligence.instrumentation.MetricsFeatureProvider;
     46 import com.android.settings.intelligence.overlay.FeatureFactory;
     47 import com.android.settings.intelligence.search.indexing.IndexingCallback;
     48 import com.android.settings.intelligence.search.savedqueries.SavedQueryController;
     49 import com.android.settings.intelligence.search.savedqueries.SavedQueryViewHolder;
     50 
     51 import java.util.List;
     52 
     53 /**
     54  * This fragment manages the lifecycle of indexing and searching.
     55  *
     56  * In onCreate, the indexing process is initiated in DatabaseIndexingManager.
     57  * While the indexing is happening, loaders are blocked from accessing the database, but the user
     58  * is free to start typing their query.
     59  *
     60  * When the indexing is complete, the fragment gets a callback to initialize the loaders and search
     61  * the query if the user has entered text.
     62  */
     63 public class SearchFragment extends Fragment implements SearchView.OnQueryTextListener,
     64         LoaderManager.LoaderCallbacks<List<? extends SearchResult>>, IndexingCallback {
     65     private static final String TAG = "SearchFragment";
     66 
     67     // State values
     68     private static final String STATE_QUERY = "state_query";
     69     private static final String STATE_SHOWING_SAVED_QUERY = "state_showing_saved_query";
     70     private static final String STATE_NEVER_ENTERED_QUERY = "state_never_entered_query";
     71 
     72     public static final class SearchLoaderId {
     73         // Search Query IDs
     74         public static final int SEARCH_RESULT = 1;
     75 
     76         // Saved Query IDs
     77         public static final int SAVE_QUERY_TASK = 2;
     78         public static final int REMOVE_QUERY_TASK = 3;
     79         public static final int SAVED_QUERIES = 4;
     80     }
     81 
     82     @VisibleForTesting
     83     String mQuery;
     84 
     85     private boolean mNeverEnteredQuery = true;
     86     private long mEnterQueryTimestampMs;
     87 
     88     @VisibleForTesting
     89     boolean mShowingSavedQuery;
     90     private MetricsFeatureProvider mMetricsFeatureProvider;
     91     @VisibleForTesting
     92     SavedQueryController mSavedQueryController;
     93 
     94     @VisibleForTesting
     95     SearchFeatureProvider mSearchFeatureProvider;
     96 
     97     @VisibleForTesting
     98     SearchResultsAdapter mSearchAdapter;
     99 
    100     @VisibleForTesting
    101     RecyclerView mResultsRecyclerView;
    102     @VisibleForTesting
    103     SearchView mSearchView;
    104     @VisibleForTesting
    105     LinearLayout mNoResultsView;
    106 
    107     @VisibleForTesting
    108     final RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
    109         @Override
    110         public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
    111             if (dy != 0) {
    112                 hideKeyboard();
    113             }
    114         }
    115     };
    116 
    117     @Override
    118     public void onAttach(Context context) {
    119         super.onAttach(context);
    120         mSearchFeatureProvider = FeatureFactory.get(context).searchFeatureProvider();
    121         mMetricsFeatureProvider = FeatureFactory.get(context).metricsFeatureProvider(context);
    122     }
    123 
    124     @Override
    125     public void onCreate(Bundle savedInstanceState) {
    126         super.onCreate(savedInstanceState);
    127         long startTime = System.currentTimeMillis();
    128         setHasOptionsMenu(true);
    129 
    130         final LoaderManager loaderManager = getLoaderManager();
    131         mSearchAdapter = new SearchResultsAdapter(this /* fragment */);
    132         mSavedQueryController = new SavedQueryController(
    133                 getContext(), loaderManager, mSearchAdapter);
    134         mSearchFeatureProvider.initFeedbackButton();
    135 
    136         if (savedInstanceState != null) {
    137             mQuery = savedInstanceState.getString(STATE_QUERY);
    138             mNeverEnteredQuery = savedInstanceState.getBoolean(STATE_NEVER_ENTERED_QUERY);
    139             mShowingSavedQuery = savedInstanceState.getBoolean(STATE_SHOWING_SAVED_QUERY);
    140         } else {
    141             mShowingSavedQuery = true;
    142         }
    143         mSearchFeatureProvider.updateIndexAsync(getContext(), this /* indexingCallback */);
    144         if (SearchFeatureProvider.DEBUG) {
    145             Log.d(TAG, "onCreate spent " + (System.currentTimeMillis() - startTime) + " ms");
    146         }
    147     }
    148 
    149     @Override
    150     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    151         super.onCreateOptionsMenu(menu, inflater);
    152         mSavedQueryController.buildMenuItem(menu);
    153     }
    154 
    155     @Override
    156     public View onCreateView(LayoutInflater inflater, ViewGroup container,
    157             Bundle savedInstanceState) {
    158         final Activity activity = getActivity();
    159         final View view = inflater.inflate(R.layout.search_panel, container, false);
    160         mResultsRecyclerView = view.findViewById(R.id.list_results);
    161         mResultsRecyclerView.setAdapter(mSearchAdapter);
    162         mResultsRecyclerView.setLayoutManager(new LinearLayoutManager(activity));
    163         mResultsRecyclerView.addOnScrollListener(mScrollListener);
    164 
    165         mNoResultsView = view.findViewById(R.id.no_results_layout);
    166 
    167         final Toolbar toolbar = view.findViewById(R.id.search_toolbar);
    168         activity.setActionBar(toolbar);
    169         activity.getActionBar().setDisplayHomeAsUpEnabled(true);
    170 
    171         mSearchView = toolbar.findViewById(R.id.search_view);
    172         mSearchView.setQuery(mQuery, false /* submitQuery */);
    173         mSearchView.setOnQueryTextListener(this);
    174         mSearchView.requestFocus();
    175 
    176         return view;
    177     }
    178 
    179     @Override
    180     public void onStart() {
    181         super.onStart();
    182         mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.OPEN_SEARCH_PAGE);
    183     }
    184 
    185     @Override
    186     public void onResume() {
    187         super.onResume();
    188         Context appContext = getContext().getApplicationContext();
    189         if (mSearchFeatureProvider.isSmartSearchRankingEnabled(appContext)) {
    190             mSearchFeatureProvider.searchRankingWarmup(appContext);
    191         }
    192         requery();
    193     }
    194 
    195     @Override
    196     public void onStop() {
    197         super.onStop();
    198         mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.LEAVE_SEARCH_PAGE);
    199         final Activity activity = getActivity();
    200         if (activity != null && activity.isFinishing()) {
    201             if (mNeverEnteredQuery) {
    202                 mMetricsFeatureProvider.logEvent(
    203                         SettingsIntelligenceEvent.LEAVE_SEARCH_WITHOUT_QUERY);
    204             }
    205         }
    206     }
    207 
    208     @Override
    209     public void onSaveInstanceState(Bundle outState) {
    210         super.onSaveInstanceState(outState);
    211         outState.putString(STATE_QUERY, mQuery);
    212         outState.putBoolean(STATE_NEVER_ENTERED_QUERY, mNeverEnteredQuery);
    213         outState.putBoolean(STATE_SHOWING_SAVED_QUERY, mShowingSavedQuery);
    214     }
    215 
    216     @Override
    217     public boolean onQueryTextChange(String query) {
    218         if (TextUtils.equals(query, mQuery)) {
    219             return true;
    220         }
    221         mEnterQueryTimestampMs = System.currentTimeMillis();
    222         final boolean isEmptyQuery = TextUtils.isEmpty(query);
    223 
    224         // Hide no-results-view when the new query is not a super-string of the previous
    225         if (mQuery != null
    226                 && mNoResultsView.getVisibility() == View.VISIBLE
    227                 && query.length() < mQuery.length()) {
    228             mNoResultsView.setVisibility(View.GONE);
    229         }
    230 
    231         mNeverEnteredQuery = false;
    232         mQuery = query;
    233 
    234         // If indexing is not finished, register the query text, but don't search.
    235         if (!mSearchFeatureProvider.isIndexingComplete(getActivity())) {
    236             return true;
    237         }
    238 
    239         if (isEmptyQuery) {
    240             final LoaderManager loaderManager = getLoaderManager();
    241             loaderManager.destroyLoader(SearchLoaderId.SEARCH_RESULT);
    242             mShowingSavedQuery = true;
    243             mSavedQueryController.loadSavedQueries();
    244             mSearchFeatureProvider.hideFeedbackButton(getView());
    245         } else {
    246             mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.PERFORM_SEARCH);
    247             restartLoaders();
    248         }
    249 
    250         return true;
    251     }
    252 
    253     @Override
    254     public boolean onQueryTextSubmit(String query) {
    255         // Save submitted query.
    256         mSavedQueryController.saveQuery(mQuery);
    257         hideKeyboard();
    258         return true;
    259     }
    260 
    261     @Override
    262     public Loader<List<? extends SearchResult>> onCreateLoader(int id, Bundle args) {
    263         final Activity activity = getActivity();
    264 
    265         switch (id) {
    266             case SearchLoaderId.SEARCH_RESULT:
    267                 return mSearchFeatureProvider.getSearchResultLoader(activity, mQuery);
    268             default:
    269                 return null;
    270         }
    271     }
    272 
    273     @Override
    274     public void onLoadFinished(Loader<List<? extends SearchResult>> loader,
    275             List<? extends SearchResult> data) {
    276         mSearchAdapter.postSearchResults(data);
    277     }
    278 
    279     @Override
    280     public void onLoaderReset(Loader<List<? extends SearchResult>> loader) {
    281     }
    282 
    283     /**
    284      * Gets called when Indexing is completed.
    285      */
    286     @Override
    287     public void onIndexingFinished() {
    288         if (getActivity() == null) {
    289             return;
    290         }
    291         if (mShowingSavedQuery) {
    292             mSavedQueryController.loadSavedQueries();
    293         } else {
    294             final LoaderManager loaderManager = getLoaderManager();
    295             loaderManager.initLoader(SearchLoaderId.SEARCH_RESULT, null /* args */,
    296                     this /* callback */);
    297         }
    298 
    299         requery();
    300     }
    301 
    302     public List<SearchResult> getSearchResults() {
    303         return mSearchAdapter.getSearchResults();
    304     }
    305 
    306     public void onSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result) {
    307         logSearchResultClicked(resultViewHolder, result);
    308         mSearchFeatureProvider.searchResultClicked(getContext(), mQuery, result);
    309         mSavedQueryController.saveQuery(mQuery);
    310     }
    311 
    312     public void onSearchResultsDisplayed(int resultCount) {
    313         final long queryToResultLatencyMs = mEnterQueryTimestampMs > 0
    314                 ? System.currentTimeMillis() - mEnterQueryTimestampMs
    315                 : 0;
    316         if (resultCount == 0) {
    317             mNoResultsView.setVisibility(View.VISIBLE);
    318             mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.SHOW_SEARCH_NO_RESULT,
    319                     queryToResultLatencyMs);
    320             EventLog.writeEvent(90204 /* settings_latency*/, 1 /* query_to_result_latency */,
    321                     (int) queryToResultLatencyMs);
    322         } else {
    323             mNoResultsView.setVisibility(View.GONE);
    324             mResultsRecyclerView.scrollToPosition(0);
    325             mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.SHOW_SEARCH_RESULT,
    326                     queryToResultLatencyMs);
    327         }
    328         mSearchFeatureProvider.showFeedbackButton(this, getView());
    329     }
    330 
    331     public void onSavedQueryClicked(SavedQueryViewHolder vh, CharSequence query) {
    332         final String queryString = query.toString();
    333         mMetricsFeatureProvider.logEvent(vh.getClickActionMetricName());
    334         mSearchView.setQuery(queryString, false /* submit */);
    335         onQueryTextChange(queryString);
    336     }
    337 
    338     private void restartLoaders() {
    339         mShowingSavedQuery = false;
    340         final LoaderManager loaderManager = getLoaderManager();
    341         loaderManager.restartLoader(
    342                 SearchLoaderId.SEARCH_RESULT, null /* args */, this /* callback */);
    343     }
    344 
    345     public String getQuery() {
    346         return mQuery;
    347     }
    348 
    349     private void requery() {
    350         if (TextUtils.isEmpty(mQuery)) {
    351             return;
    352         }
    353         final String query = mQuery;
    354         mQuery = "";
    355         onQueryTextChange(query);
    356     }
    357 
    358     private void hideKeyboard() {
    359         final Activity activity = getActivity();
    360         if (activity != null) {
    361             View view = activity.getCurrentFocus();
    362             InputMethodManager imm = (InputMethodManager)
    363                     activity.getSystemService(Context.INPUT_METHOD_SERVICE);
    364             imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
    365         }
    366 
    367         if (mResultsRecyclerView != null) {
    368             mResultsRecyclerView.requestFocus();
    369         }
    370     }
    371 
    372     private void logSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result) {
    373         final int resultType = resultViewHolder.getClickActionMetricName();
    374         final int resultCount = mSearchAdapter.getItemCount();
    375         final int resultRank = resultViewHolder.getAdapterPosition();
    376         mMetricsFeatureProvider.logSearchResultClick(result, mQuery, resultType, resultCount,
    377                 resultRank);
    378     }
    379 }
    380