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 androidx.annotation.VisibleForTesting; 29 import androidx.recyclerview.widget.LinearLayoutManager; 30 import androidx.recyclerview.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 onAttach(Context context)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 onCreate(Bundle savedInstanceState)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 onCreateOptionsMenu(Menu menu, MenuInflater inflater)150 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 151 super.onCreateOptionsMenu(menu, inflater); 152 mSavedQueryController.buildMenuItem(menu); 153 } 154 155 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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 onStart()180 public void onStart() { 181 super.onStart(); 182 mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.OPEN_SEARCH_PAGE); 183 } 184 185 @Override onResume()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 onStop()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 onSaveInstanceState(Bundle outState)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 onQueryTextChange(String query)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 onQueryTextSubmit(String query)254 public boolean onQueryTextSubmit(String query) { 255 // Save submitted query. 256 mSavedQueryController.saveQuery(mQuery); 257 hideKeyboard(); 258 return true; 259 } 260 261 @Override onCreateLoader(int id, Bundle args)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 onLoadFinished(Loader<List<? extends SearchResult>> loader, List<? extends SearchResult> data)274 public void onLoadFinished(Loader<List<? extends SearchResult>> loader, 275 List<? extends SearchResult> data) { 276 mSearchAdapter.postSearchResults(data); 277 } 278 279 @Override onLoaderReset(Loader<List<? extends SearchResult>> loader)280 public void onLoaderReset(Loader<List<? extends SearchResult>> loader) { 281 } 282 283 /** 284 * Gets called when Indexing is completed. 285 */ 286 @Override onIndexingFinished()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 getSearchResults()302 public List<SearchResult> getSearchResults() { 303 return mSearchAdapter.getSearchResults(); 304 } 305 onSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result)306 public void onSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result) { 307 logSearchResultClicked(resultViewHolder, result); 308 mSearchFeatureProvider.searchResultClicked(getContext(), mQuery, result); 309 mSavedQueryController.saveQuery(mQuery); 310 } 311 onSearchResultsDisplayed(int resultCount)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 onSavedQueryClicked(SavedQueryViewHolder vh, CharSequence query)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 restartLoaders()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 getQuery()345 public String getQuery() { 346 return mQuery; 347 } 348 requery()349 private void requery() { 350 if (TextUtils.isEmpty(mQuery)) { 351 return; 352 } 353 final String query = mQuery; 354 mQuery = ""; 355 onQueryTextChange(query); 356 } 357 hideKeyboard()358 private void hideKeyboard() { 359 final Activity activity = getActivity(); 360 if (activity != null) { 361 View view = activity.getCurrentFocus(); 362 if (view != null) { 363 InputMethodManager imm = (InputMethodManager) 364 activity.getSystemService(Context.INPUT_METHOD_SERVICE); 365 imm.hideSoftInputFromWindow(view.getWindowToken(), 0); 366 } 367 } 368 369 if (mResultsRecyclerView != null) { 370 mResultsRecyclerView.requestFocus(); 371 } 372 } 373 logSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result)374 private void logSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result) { 375 final int resultType = resultViewHolder.getClickActionMetricName(); 376 final int resultCount = mSearchAdapter.getItemCount(); 377 final int resultRank = resultViewHolder.getAdapterPosition(); 378 mMetricsFeatureProvider.logSearchResultClick(result, mQuery, resultType, resultCount, 379 resultRank); 380 } 381 } 382