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