/* * Copyright (C) 2014 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.dashboard; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.Cursor; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.ListView; import android.widget.SearchView; import android.widget.TextView; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.MetricsProto.MetricsEvent; import com.android.settings.InstrumentedFragment; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.Utils; import com.android.settings.search.Index; import java.util.HashMap; public class SearchResultsSummary extends InstrumentedFragment { private static final String LOG_TAG = "SearchResultsSummary"; private static final String EMPTY_QUERY = ""; private static char ELLIPSIS = '\u2026'; private static final String SAVE_KEY_SHOW_RESULTS = ":settings:show_results"; private SearchView mSearchView; private ListView mResultsListView; private SearchResultsAdapter mResultsAdapter; private UpdateSearchResultsTask mUpdateSearchResultsTask; private ListView mSuggestionsListView; private SuggestionsAdapter mSuggestionsAdapter; private UpdateSuggestionsTask mUpdateSuggestionsTask; private ViewGroup mLayoutSuggestions; private ViewGroup mLayoutResults; private String mQuery; private boolean mShowResults; /** * A basic AsyncTask for updating the query results cursor */ private class UpdateSearchResultsTask extends AsyncTask<String, Void, Cursor> { @Override protected Cursor doInBackground(String... params) { return Index.getInstance(getActivity()).search(params[0]); } @Override protected void onPostExecute(Cursor cursor) { if (!isCancelled()) { MetricsLogger.action(getContext(), MetricsEvent.ACTION_SEARCH_RESULTS, cursor.getCount()); setResultsCursor(cursor); setResultsVisibility(cursor.getCount() > 0); } else if (cursor != null) { cursor.close(); } } } /** * A basic AsyncTask for updating the suggestions cursor */ private class UpdateSuggestionsTask extends AsyncTask<String, Void, Cursor> { @Override protected Cursor doInBackground(String... params) { return Index.getInstance(getActivity()).getSuggestions(params[0]); } @Override protected void onPostExecute(Cursor cursor) { if (!isCancelled()) { setSuggestionsCursor(cursor); setSuggestionsVisibility(cursor.getCount() > 0); } else if (cursor != null) { cursor.close(); } } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mResultsAdapter = new SearchResultsAdapter(getActivity()); mSuggestionsAdapter = new SuggestionsAdapter(getActivity()); if (savedInstanceState != null) { mShowResults = savedInstanceState.getBoolean(SAVE_KEY_SHOW_RESULTS); } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(SAVE_KEY_SHOW_RESULTS, mShowResults); } @Override public void onStop() { super.onStop(); clearSuggestions(); clearResults(); } @Override public void onDestroy() { mResultsListView = null; mResultsAdapter = null; mUpdateSearchResultsTask = null; mSuggestionsListView = null; mSuggestionsAdapter = null; mUpdateSuggestionsTask = null; mSearchView = null; super.onDestroy(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.search_panel, container, false); mLayoutSuggestions = (ViewGroup) view.findViewById(R.id.layout_suggestions); mLayoutResults = (ViewGroup) view.findViewById(R.id.layout_results); mResultsListView = (ListView) view.findViewById(R.id.list_results); mResultsListView.setAdapter(mResultsAdapter); mResultsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { // We have a header, so we need to decrement the position by one position--; // Some Monkeys could create a case where they were probably clicking on the // List Header and thus the position passed was "0" and then by decrement was "-1" if (position < 0) { return; } final Cursor cursor = mResultsAdapter.mCursor; cursor.moveToPosition(position); final String className = cursor.getString(Index.COLUMN_INDEX_CLASS_NAME); final String screenTitle = cursor.getString(Index.COLUMN_INDEX_SCREEN_TITLE); final String action = cursor.getString(Index.COLUMN_INDEX_INTENT_ACTION); final String key = cursor.getString(Index.COLUMN_INDEX_KEY); final SettingsActivity sa = (SettingsActivity) getActivity(); sa.needToRevertToInitialFragment(); if (TextUtils.isEmpty(action)) { Bundle args = new Bundle(); args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key); Utils.startWithFragment(sa, className, args, null, 0, -1, screenTitle); } else { final Intent intent = new Intent(action); final String targetPackage = cursor.getString( Index.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE); final String targetClass = cursor.getString( Index.COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS); if (!TextUtils.isEmpty(targetPackage) && !TextUtils.isEmpty(targetClass)) { final ComponentName component = new ComponentName(targetPackage, targetClass); intent.setComponent(component); } intent.putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key); sa.startActivity(intent); } saveQueryToDatabase(); } }); mResultsListView.addHeaderView( LayoutInflater.from(getActivity()).inflate( R.layout.search_panel_results_header, mResultsListView, false), null, false); mSuggestionsListView = (ListView) view.findViewById(R.id.list_suggestions); mSuggestionsListView.setAdapter(mSuggestionsAdapter); mSuggestionsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { // We have a header, so we need to decrement the position by one position--; // Some Monkeys could create a case where they were probably clicking on the // List Header and thus the position passed was "0" and then by decrement was "-1" if (position < 0) { return; } final Cursor cursor = mSuggestionsAdapter.mCursor; cursor.moveToPosition(position); mShowResults = true; mQuery = cursor.getString(0); mSearchView.setQuery(mQuery, false); } }); mSuggestionsListView.addHeaderView( LayoutInflater.from(getActivity()).inflate( R.layout.search_panel_suggestions_header, mSuggestionsListView, false), null, false); return view; } @Override protected int getMetricsCategory() { return MetricsEvent.DASHBOARD_SEARCH_RESULTS; } @Override public void onResume() { super.onResume(); if (!mShowResults) { showSomeSuggestions(); } } public void setSearchView(SearchView searchView) { mSearchView = searchView; } private void setSuggestionsVisibility(boolean visible) { if (mLayoutSuggestions != null) { mLayoutSuggestions.setVisibility(visible ? View.VISIBLE : View.GONE); } } private void setResultsVisibility(boolean visible) { if (mLayoutResults != null) { mLayoutResults.setVisibility(visible ? View.VISIBLE : View.GONE); } } private void saveQueryToDatabase() { Index.getInstance(getActivity()).addSavedQuery(mQuery); } public boolean onQueryTextSubmit(String query) { mQuery = getFilteredQueryString(query); mShowResults = true; setSuggestionsVisibility(false); updateSearchResults(); saveQueryToDatabase(); return false; } public boolean onQueryTextChange(String query) { final String newQuery = getFilteredQueryString(query); mQuery = newQuery; if (TextUtils.isEmpty(mQuery)) { mShowResults = false; setResultsVisibility(false); updateSuggestions(); } else { mShowResults = true; setSuggestionsVisibility(false); updateSearchResults(); } return true; } public void showSomeSuggestions() { setResultsVisibility(false); mQuery = EMPTY_QUERY; updateSuggestions(); } private void clearSuggestions() { if (mUpdateSuggestionsTask != null) { mUpdateSuggestionsTask.cancel(false); mUpdateSuggestionsTask = null; } setSuggestionsCursor(null); setSuggestionsVisibility(false); } private void setSuggestionsCursor(Cursor cursor) { if (mSuggestionsAdapter == null) { return; } Cursor oldCursor = mSuggestionsAdapter.swapCursor(cursor); if (oldCursor != null) { oldCursor.close(); } } private void clearResults() { if (mUpdateSearchResultsTask != null) { mUpdateSearchResultsTask.cancel(false); mUpdateSearchResultsTask = null; } setResultsCursor(null); setResultsVisibility(false); } private void setResultsCursor(Cursor cursor) { if (mResultsAdapter == null) { return; } Cursor oldCursor = mResultsAdapter.swapCursor(cursor); if (oldCursor != null) { oldCursor.close(); } } private String getFilteredQueryString(CharSequence query) { if (query == null) { return null; } final StringBuilder filtered = new StringBuilder(); for (int n = 0; n < query.length(); n++) { char c = query.charAt(n); if (!Character.isLetterOrDigit(c) && !Character.isSpaceChar(c)) { continue; } filtered.append(c); } return filtered.toString(); } private void clearAllTasks() { if (mUpdateSearchResultsTask != null) { mUpdateSearchResultsTask.cancel(false); mUpdateSearchResultsTask = null; } if (mUpdateSuggestionsTask != null) { mUpdateSuggestionsTask.cancel(false); mUpdateSuggestionsTask = null; } } private void updateSuggestions() { clearAllTasks(); if (mQuery == null) { setSuggestionsCursor(null); } else { mUpdateSuggestionsTask = new UpdateSuggestionsTask(); mUpdateSuggestionsTask.execute(mQuery); } } private void updateSearchResults() { clearAllTasks(); if (TextUtils.isEmpty(mQuery)) { setResultsVisibility(false); setResultsCursor(null); } else { mUpdateSearchResultsTask = new UpdateSearchResultsTask(); mUpdateSearchResultsTask.execute(mQuery); } } private static class SuggestionItem { public String query; public SuggestionItem(String query) { this.query = query; } } private static class SuggestionsAdapter extends BaseAdapter { private static final int COLUMN_SUGGESTION_QUERY = 0; private static final int COLUMN_SUGGESTION_TIMESTAMP = 1; private Context mContext; private Cursor mCursor; private LayoutInflater mInflater; private boolean mDataValid = false; public SuggestionsAdapter(Context context) { mContext = context; mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mDataValid = false; } public Cursor swapCursor(Cursor newCursor) { if (newCursor == mCursor) { return null; } Cursor oldCursor = mCursor; mCursor = newCursor; if (newCursor != null) { mDataValid = true; notifyDataSetChanged(); } else { mDataValid = false; notifyDataSetInvalidated(); } return oldCursor; } @Override public int getCount() { if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0; return mCursor.getCount(); } @Override public Object getItem(int position) { if (mDataValid && mCursor.moveToPosition(position)) { final String query = mCursor.getString(COLUMN_SUGGESTION_QUERY); return new SuggestionItem(query); } return null; } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (!mDataValid && convertView == null) { throw new IllegalStateException( "this should only be called when the cursor is valid"); } if (!mCursor.moveToPosition(position)) { throw new IllegalStateException("couldn't move cursor to position " + position); } View view; if (convertView == null) { view = mInflater.inflate(R.layout.search_suggestion_item, parent, false); } else { view = convertView; } TextView query = (TextView) view.findViewById(R.id.title); SuggestionItem item = (SuggestionItem) getItem(position); query.setText(item.query); return view; } } private static class SearchResult { public Context context; public String title; public String summaryOn; public String summaryOff; public String entries; public int iconResId; public String key; public SearchResult(Context context, String title, String summaryOn, String summaryOff, String entries, int iconResId, String key) { this.context = context; this.title = title; this.summaryOn = summaryOn; this.summaryOff = summaryOff; this.entries = entries; this.iconResId = iconResId; this.key = key; } } private static class SearchResultsAdapter extends BaseAdapter { private Context mContext; private Cursor mCursor; private LayoutInflater mInflater; private boolean mDataValid; private HashMap<String, Context> mContextMap = new HashMap<String, Context>(); private static final String PERCENT_RECLACE = "%s"; private static final String DOLLAR_REPLACE = "$s"; public SearchResultsAdapter(Context context) { mContext = context; mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mDataValid = false; } public Cursor swapCursor(Cursor newCursor) { if (newCursor == mCursor) { return null; } Cursor oldCursor = mCursor; mCursor = newCursor; if (newCursor != null) { mDataValid = true; notifyDataSetChanged(); } else { mDataValid = false; notifyDataSetInvalidated(); } return oldCursor; } @Override public int getCount() { if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0; return mCursor.getCount(); } @Override public Object getItem(int position) { if (mDataValid && mCursor.moveToPosition(position)) { final String title = mCursor.getString(Index.COLUMN_INDEX_TITLE); final String summaryOn = mCursor.getString(Index.COLUMN_INDEX_SUMMARY_ON); final String summaryOff = mCursor.getString(Index.COLUMN_INDEX_SUMMARY_OFF); final String entries = mCursor.getString(Index.COLUMN_INDEX_ENTRIES); final String iconResStr = mCursor.getString(Index.COLUMN_INDEX_ICON); final String className = mCursor.getString( Index.COLUMN_INDEX_CLASS_NAME); final String packageName = mCursor.getString( Index.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE); final String key = mCursor.getString( Index.COLUMN_INDEX_KEY); Context packageContext; if (TextUtils.isEmpty(className) && !TextUtils.isEmpty(packageName)) { packageContext = mContextMap.get(packageName); if (packageContext == null) { try { packageContext = mContext.createPackageContext(packageName, 0); } catch (PackageManager.NameNotFoundException e) { Log.e(LOG_TAG, "Cannot create Context for package: " + packageName); return null; } mContextMap.put(packageName, packageContext); } } else { packageContext = mContext; } final int iconResId = TextUtils.isEmpty(iconResStr) ? R.drawable.empty_icon : Integer.parseInt(iconResStr); return new SearchResult(packageContext, title, summaryOn, summaryOff, entries, iconResId, key); } return null; } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (!mDataValid && convertView == null) { throw new IllegalStateException( "this should only be called when the cursor is valid"); } if (!mCursor.moveToPosition(position)) { throw new IllegalStateException("couldn't move cursor to position " + position); } View view; TextView textTitle; ImageView imageView; if (convertView == null) { view = mInflater.inflate(R.layout.search_result_item, parent, false); } else { view = convertView; } textTitle = (TextView) view.findViewById(R.id.title); imageView = (ImageView) view.findViewById(R.id.icon); final SearchResult result = (SearchResult) getItem(position); textTitle.setText(result.title); if (result.iconResId != R.drawable.empty_icon) { final Context packageContext = result.context; final Drawable drawable; try { drawable = packageContext.getDrawable(result.iconResId); imageView.setImageDrawable(drawable); } catch (Resources.NotFoundException nfe) { // Not much we can do except logging Log.e(LOG_TAG, "Cannot load Drawable for " + result.title); } } else { imageView.setImageDrawable(null); imageView.setBackgroundResource(R.drawable.empty_icon); } return view; } } }