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