| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| */ |
| |
| package com.android.settings.intelligence.search; |
| |
| import static com.android.settings.intelligence.nano.SettingsIntelligenceLogProto.SettingsIntelligenceEvent; |
| |
| import android.app.Activity; |
| import android.content.Context; |
| import android.os.Bundle; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.cardview.widget.CardView; |
| import androidx.loader.content.Loader; |
| import androidx.loader.app.LoaderManager; |
| import androidx.recyclerview.widget.LinearLayoutManager; |
| import androidx.recyclerview.widget.RecyclerView; |
| import androidx.fragment.app.Fragment; |
| import android.text.TextUtils; |
| import android.util.EventLog; |
| import android.util.Log; |
| import android.view.LayoutInflater; |
| import android.view.Menu; |
| import android.view.MenuInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.inputmethod.InputMethodManager; |
| import android.widget.LinearLayout; |
| import android.widget.SearchView; |
| import android.widget.Toolbar; |
| |
| import com.android.settings.intelligence.R; |
| import com.android.settings.intelligence.instrumentation.MetricsFeatureProvider; |
| import com.android.settings.intelligence.overlay.FeatureFactory; |
| import com.android.settings.intelligence.search.indexing.IndexingCallback; |
| import com.android.settings.intelligence.search.savedqueries.SavedQueryController; |
| import com.android.settings.intelligence.search.savedqueries.SavedQueryViewHolder; |
| |
| import java.util.List; |
| |
| /** |
| * This fragment manages the lifecycle of indexing and searching. |
| * |
| * In onCreate, the indexing process is initiated in DatabaseIndexingManager. |
| * While the indexing is happening, loaders are blocked from accessing the database, but the user |
| * is free to start typing their query. |
| * |
| * When the indexing is complete, the fragment gets a callback to initialize the loaders and search |
| * the query if the user has entered text. |
| */ |
| public class SearchFragment extends Fragment implements SearchView.OnQueryTextListener, |
| LoaderManager.LoaderCallbacks<List<? extends SearchResult>>, IndexingCallback { |
| private static final String TAG = "SearchFragment"; |
| |
| @VisibleForTesting |
| String mQuery; |
| |
| private boolean mNeverEnteredQuery = true; |
| private long mEnterQueryTimestampMs; |
| |
| @VisibleForTesting |
| boolean mShowingSavedQuery; |
| private MetricsFeatureProvider mMetricsFeatureProvider; |
| @VisibleForTesting |
| SavedQueryController mSavedQueryController; |
| |
| @VisibleForTesting |
| SearchFeatureProvider mSearchFeatureProvider; |
| |
| @VisibleForTesting |
| SearchResultsAdapter mSearchAdapter; |
| |
| @VisibleForTesting |
| RecyclerView mResultsRecyclerView; |
| @VisibleForTesting |
| SearchView mSearchView; |
| @VisibleForTesting |
| LinearLayout mNoResultsView; |
| |
| @VisibleForTesting |
| final RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() { |
| @Override |
| public void onScrolled(RecyclerView recyclerView, int dx, int dy) { |
| if (dy != 0) { |
| hideKeyboard(); |
| } |
| } |
| }; |
| |
| @Override |
| public void onAttach(Context context) { |
| super.onAttach(context); |
| mSearchFeatureProvider = FeatureFactory.get(context).searchFeatureProvider(); |
| mMetricsFeatureProvider = FeatureFactory.get(context).metricsFeatureProvider(context); |
| } |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| long startTime = System.currentTimeMillis(); |
| setHasOptionsMenu(true); |
| |
| final LoaderManager loaderManager = getLoaderManager(); |
| mSearchAdapter = new SearchResultsAdapter(this /* fragment */); |
| mSavedQueryController = new SavedQueryController( |
| getContext(), loaderManager, mSearchAdapter); |
| mSearchFeatureProvider.initFeedbackButton(); |
| |
| if (savedInstanceState != null) { |
| mQuery = savedInstanceState.getString(SearchCommon.STATE_QUERY); |
| mNeverEnteredQuery = savedInstanceState.getBoolean(SearchCommon.STATE_NEVER_ENTERED_QUERY); |
| mShowingSavedQuery = savedInstanceState.getBoolean(SearchCommon.STATE_SHOWING_SAVED_QUERY); |
| } else { |
| mShowingSavedQuery = true; |
| } |
| mSearchFeatureProvider.updateIndexAsync(getContext(), this /* indexingCallback */); |
| if (SearchFeatureProvider.DEBUG) { |
| Log.d(TAG, "onCreate spent " + (System.currentTimeMillis() - startTime) + " ms"); |
| } |
| } |
| |
| @Override |
| public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { |
| super.onCreateOptionsMenu(menu, inflater); |
| mSavedQueryController.buildMenuItem(menu); |
| } |
| |
| @Override |
| public View onCreateView(LayoutInflater inflater, ViewGroup container, |
| Bundle savedInstanceState) { |
| final Activity activity = getActivity(); |
| final View view = inflater.inflate(R.layout.search_panel, container, false); |
| mResultsRecyclerView = view.findViewById(R.id.list_results); |
| mResultsRecyclerView.setAdapter(mSearchAdapter); |
| mResultsRecyclerView.setLayoutManager(new LinearLayoutManager(activity)); |
| mResultsRecyclerView.addOnScrollListener(mScrollListener); |
| |
| mNoResultsView = view.findViewById(R.id.no_results_layout); |
| |
| final CardView cardView = view.findViewById(R.id.search_bar); |
| cardView.setBackgroundResource(R.drawable.search_bar_selected_background); |
| |
| final Toolbar toolbar = view.findViewById(R.id.search_toolbar); |
| activity.setActionBar(toolbar); |
| activity.getActionBar().setDisplayHomeAsUpEnabled(true); |
| |
| mSearchView = toolbar.findViewById(R.id.search_view); |
| mSearchView.setQuery(mQuery, false /* submitQuery */); |
| mSearchView.setOnQueryTextListener(this); |
| mSearchView.requestFocus(); |
| |
| return view; |
| } |
| |
| @Override |
| public void onStart() { |
| super.onStart(); |
| mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.OPEN_SEARCH_PAGE); |
| } |
| |
| @Override |
| public void onResume() { |
| super.onResume(); |
| Context appContext = getContext().getApplicationContext(); |
| if (mSearchFeatureProvider.isSmartSearchRankingEnabled(appContext)) { |
| mSearchFeatureProvider.searchRankingWarmup(appContext); |
| } |
| requery(); |
| } |
| |
| @Override |
| public void onStop() { |
| super.onStop(); |
| mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.LEAVE_SEARCH_PAGE); |
| final Activity activity = getActivity(); |
| if (activity != null && activity.isFinishing()) { |
| if (mNeverEnteredQuery) { |
| mMetricsFeatureProvider.logEvent( |
| SettingsIntelligenceEvent.LEAVE_SEARCH_WITHOUT_QUERY); |
| } |
| } |
| } |
| |
| @Override |
| public void onSaveInstanceState(Bundle outState) { |
| super.onSaveInstanceState(outState); |
| outState.putString(SearchCommon.STATE_QUERY, mQuery); |
| outState.putBoolean(SearchCommon.STATE_NEVER_ENTERED_QUERY, mNeverEnteredQuery); |
| outState.putBoolean(SearchCommon.STATE_SHOWING_SAVED_QUERY, mShowingSavedQuery); |
| } |
| |
| @Override |
| public boolean onQueryTextChange(String query) { |
| if (TextUtils.equals(query, mQuery)) { |
| return true; |
| } |
| mEnterQueryTimestampMs = System.currentTimeMillis(); |
| final boolean isEmptyQuery = TextUtils.isEmpty(query); |
| |
| // Hide no-results-view when the new query is not a super-string of the previous |
| if (mQuery != null |
| && mNoResultsView.getVisibility() == View.VISIBLE |
| && query.length() < mQuery.length()) { |
| mNoResultsView.setVisibility(View.GONE); |
| } |
| |
| mNeverEnteredQuery = false; |
| mQuery = query; |
| |
| // If indexing is not finished, register the query text, but don't search. |
| if (!mSearchFeatureProvider.isIndexingComplete(getActivity())) { |
| return true; |
| } |
| |
| if (isEmptyQuery) { |
| final LoaderManager loaderManager = getLoaderManager(); |
| loaderManager.destroyLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT); |
| mShowingSavedQuery = true; |
| mSavedQueryController.loadSavedQueries(); |
| mSearchFeatureProvider.hideFeedbackButton(getView()); |
| } else { |
| mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.PERFORM_SEARCH); |
| restartLoaders(); |
| } |
| |
| return true; |
| } |
| |
| @Override |
| public boolean onQueryTextSubmit(String query) { |
| // Save submitted query. |
| mSavedQueryController.saveQuery(mQuery); |
| hideKeyboard(); |
| return true; |
| } |
| |
| @Override |
| public Loader<List<? extends SearchResult>> onCreateLoader(int id, Bundle args) { |
| final Activity activity = getActivity(); |
| |
| switch (id) { |
| case SearchCommon.SearchLoaderId.SEARCH_RESULT: |
| return mSearchFeatureProvider.getSearchResultLoader(activity, mQuery); |
| default: |
| return null; |
| } |
| } |
| |
| @Override |
| public void onLoadFinished(Loader<List<? extends SearchResult>> loader, |
| List<? extends SearchResult> data) { |
| mSearchAdapter.postSearchResults(data); |
| } |
| |
| @Override |
| public void onLoaderReset(Loader<List<? extends SearchResult>> loader) { |
| } |
| |
| /** |
| * Gets called when Indexing is completed. |
| */ |
| @Override |
| public void onIndexingFinished() { |
| if (getActivity() == null) { |
| return; |
| } |
| if (mShowingSavedQuery) { |
| mSavedQueryController.loadSavedQueries(); |
| } else { |
| final LoaderManager loaderManager = getLoaderManager(); |
| loaderManager.initLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT, null /* args */, |
| this /* callback */); |
| } |
| |
| requery(); |
| } |
| |
| public List<SearchResult> getSearchResults() { |
| return mSearchAdapter.getSearchResults(); |
| } |
| |
| public void onSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result) { |
| logSearchResultClicked(resultViewHolder, result); |
| mSearchFeatureProvider.searchResultClicked(getContext(), mQuery, result); |
| mSavedQueryController.saveQuery(mQuery); |
| } |
| |
| public void onSearchResultsDisplayed(int resultCount) { |
| final long queryToResultLatencyMs = mEnterQueryTimestampMs > 0 |
| ? System.currentTimeMillis() - mEnterQueryTimestampMs |
| : 0; |
| if (resultCount == 0) { |
| mNoResultsView.setVisibility(View.VISIBLE); |
| mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.SHOW_SEARCH_NO_RESULT, |
| queryToResultLatencyMs); |
| EventLog.writeEvent(90204 /* settings_latency*/, 1 /* query_to_result_latency */, |
| (int) queryToResultLatencyMs); |
| } else { |
| mNoResultsView.setVisibility(View.GONE); |
| mResultsRecyclerView.scrollToPosition(0); |
| mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.SHOW_SEARCH_RESULT, |
| queryToResultLatencyMs); |
| } |
| mSearchFeatureProvider.showFeedbackButton(this, getView()); |
| } |
| |
| public void onSavedQueryClicked(SavedQueryViewHolder vh, CharSequence query) { |
| final String queryString = query.toString(); |
| mMetricsFeatureProvider.logEvent(vh.getClickActionMetricName()); |
| mSearchView.setQuery(queryString, false /* submit */); |
| onQueryTextChange(queryString); |
| } |
| |
| private void restartLoaders() { |
| mShowingSavedQuery = false; |
| final LoaderManager loaderManager = getLoaderManager(); |
| loaderManager.restartLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT, |
| null /* args */, this /* callback */); |
| } |
| |
| public String getQuery() { |
| return mQuery; |
| } |
| |
| private void requery() { |
| if (TextUtils.isEmpty(mQuery)) { |
| return; |
| } |
| final String query = mQuery; |
| mQuery = ""; |
| onQueryTextChange(query); |
| } |
| |
| private void hideKeyboard() { |
| final Activity activity = getActivity(); |
| if (activity != null) { |
| View view = activity.getCurrentFocus(); |
| if (view != null) { |
| InputMethodManager imm = (InputMethodManager) |
| activity.getSystemService(Context.INPUT_METHOD_SERVICE); |
| imm.hideSoftInputFromWindow(view.getWindowToken(), 0); |
| } |
| } |
| |
| if (mResultsRecyclerView != null) { |
| mResultsRecyclerView.requestFocus(); |
| } |
| } |
| |
| private void logSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result) { |
| final int resultType = resultViewHolder.getClickActionMetricName(); |
| final int resultCount = mSearchAdapter.getItemCount(); |
| final int resultRank = resultViewHolder.getAdapterPosition(); |
| mMetricsFeatureProvider.logSearchResultClick(result, mQuery, resultType, resultCount, |
| resultRank); |
| } |
| } |