Add saved Search queries feature

- update SearchResultsSummary fragment to have two lists:
one for Search suggestions (saved queries) and one for
Search results
- a tap on a saved query will launch that Search query
- show the list of saved queries when tapping on the SearchView
- do some fancy hidding / unhidding of the saved queries list
and results list

Change-Id: If15055ab78b0ec5eef4e543173dc7b866bd08e27
diff --git a/src/com/android/settings/SettingsActivity.java b/src/com/android/settings/SettingsActivity.java
index 964c444..4c3e2bc 100644
--- a/src/com/android/settings/SettingsActivity.java
+++ b/src/com/android/settings/SettingsActivity.java
@@ -1280,6 +1280,7 @@
             mSearchResultsFragment = (SearchResultsSummary) switchToFragment(
                     SearchResultsSummary.class.getName(), null, false, true, title, true);
         }
+        mSearchResultsFragment.setSearchView(mSearchView);
         mSearchMenuItemExpanded = true;
     }
 
diff --git a/src/com/android/settings/dashboard/SearchResultsSummary.java b/src/com/android/settings/dashboard/SearchResultsSummary.java
index a7076ea..89e33f6 100644
--- a/src/com/android/settings/dashboard/SearchResultsSummary.java
+++ b/src/com/android/settings/dashboard/SearchResultsSummary.java
@@ -28,7 +28,6 @@
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
 import android.os.Bundle;
-import android.os.Handler;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.LayoutInflater;
@@ -38,6 +37,7 @@
 import android.widget.BaseAdapter;
 import android.widget.ImageView;
 import android.widget.ListView;
+import android.widget.SearchView;
 import android.widget.TextView;
 import com.android.settings.R;
 import com.android.settings.SettingsActivity;
@@ -54,17 +54,23 @@
 
     private static final String LOG_TAG = "SearchResultsSummary";
 
+    private static final String EMPTY_QUERY = "";
     private static char ELLIPSIS = '\u2026';
 
-    private ListView mListView;
+    private SearchView mSearchView;
 
-    private SearchResultsAdapter mAdapter;
+    private ListView mResultsListView;
+    private SearchResultsAdapter mResultsAdapter;
     private UpdateSearchResultsTask mUpdateSearchResultsTask;
 
-    private String mQuery;
-    private SaveSearchQueryTask mSaveSearchQueryTask;
+    private ListView mSuggestionsListView;
+    private SuggestionsAdapter mSuggestionsAdapter;
+    private UpdateSuggestionsTask mUpdateSuggestionsTask;
 
-    private static long MAX_SAVED_SEARCH_QUERY = 5;
+    private ViewGroup mLayoutSuggestions;
+    private ViewGroup mLayoutResults;
+
+    private String mQuery;
 
     /**
      * A basic AsyncTask for updating the query results cursor
@@ -78,7 +84,7 @@
         @Override
         protected void onPostExecute(Cursor cursor) {
             if (!isCancelled()) {
-                setCursor(cursor);
+                setResultsCursor(cursor);
             } else if (cursor != null) {
                 cursor.close();
             }
@@ -86,37 +92,21 @@
     }
 
     /**
-     * A basic AsynTask for saving the Search query into the database
+     * A basic AsyncTask for updating the suggestions cursor
      */
-    private class SaveSearchQueryTask extends AsyncTask<String, Void, Long> {
+    private class UpdateSuggestionsTask extends AsyncTask<String, Void, Cursor> {
+        @Override
+        protected Cursor doInBackground(String... params) {
+            return Index.getInstance(getActivity()).getSuggestions(params[0]);
+        }
 
         @Override
-        protected Long doInBackground(String... params) {
-            final long now = new Date().getTime();
-
-            final ContentValues values = new ContentValues();
-            values.put(SavedQueriesColums.QUERY, params[0]);
-            values.put(SavedQueriesColums.TIME_STAMP, now);
-
-            SQLiteDatabase database = IndexDatabaseHelper.getInstance(
-                    getActivity()).getWritableDatabase();
-
-            long lastInsertedRowId = -1;
-            try {
-                lastInsertedRowId =
-                        database.insert(Tables.TABLE_SAVED_QUERIES, null, values);
-
-                final long delta = lastInsertedRowId - MAX_SAVED_SEARCH_QUERY;
-                if (delta > 0) {
-                    int count = database.delete(Tables.TABLE_SAVED_QUERIES, "rowId <= ?",
-                            new String[] { Long.toString(delta) });
-                    Log.d(LOG_TAG, "Deleted '" + count + "' saved Search query(ies)");
-                }
-            } catch (Exception e) {
-                Log.d(LOG_TAG, "Cannot update saved Search queries", e);
+        protected void onPostExecute(Cursor cursor) {
+            if (!isCancelled()) {
+                setSuggestionsCursor(cursor);
+            } else if (cursor != null) {
+                cursor.close();
             }
-
-            return lastInsertedRowId;
         }
     }
 
@@ -124,22 +114,30 @@
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        mAdapter = new SearchResultsAdapter(getActivity());
+        mResultsAdapter = new SearchResultsAdapter(getActivity());
+        mSuggestionsAdapter = new SuggestionsAdapter(getActivity());
     }
 
     @Override
     public void onStop() {
         super.onStop();
 
+        clearSuggestions();
         clearResults();
     }
 
     @Override
     public void onDestroy() {
-        mListView = null;
-        mAdapter = null;
+        mResultsListView = null;
+        mResultsAdapter = null;
         mUpdateSearchResultsTask = null;
 
+        mSuggestionsListView = null;
+        mSuggestionsAdapter = null;
+        mUpdateSuggestionsTask = null;
+
+        mSearchView = null;
+
         super.onDestroy();
     }
 
@@ -147,14 +145,17 @@
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
                              Bundle savedInstanceState) {
 
-        final View view = inflater.inflate(R.layout.search_results, container, false);
+        final View view = inflater.inflate(R.layout.search_panel, container, false);
 
-        mListView = (ListView) view.findViewById(R.id.list_results);
-        mListView.setAdapter(mAdapter);
-        mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+        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) {
-                final Cursor cursor = mAdapter.mCursor;
+                final Cursor cursor = mResultsAdapter.mCursor;
                 cursor.moveToPosition(position);
 
                 final String className = cursor.getString(Index.COLUMN_INDEX_CLASS_NAME);
@@ -192,33 +193,85 @@
             }
         });
 
+        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) {
+                final Cursor cursor = mSuggestionsAdapter.mCursor;
+                cursor.moveToPosition(position);
+
+                mQuery = cursor.getString(0);
+                mSearchView.setQuery(mQuery, false);
+                setSuggestionsVisibility(false);
+            }
+        });
+
         return view;
     }
 
-    private void saveQueryToDatabase() {
-        if (mSaveSearchQueryTask != null) {
-            mSaveSearchQueryTask.cancel(false);
-            mSaveSearchQueryTask = null;
-        }
-        if (!TextUtils.isEmpty(mQuery)) {
-            mSaveSearchQueryTask = new SaveSearchQueryTask();
-            mSaveSearchQueryTask.execute(mQuery);
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        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) {
-        updateSearchResults(query);
+        mQuery = getFilteredQueryString(query);
+        updateSearchResults();
         return true;
     }
 
     public boolean onQueryTextChange(String query) {
-        updateSearchResults(query);
+        mQuery = getFilteredQueryString(query);
+        updateSuggestions();
+        updateSearchResults();
         return true;
     }
 
-    public boolean onClose() {
-        clearResults();
-        return false;
+    public void showSomeSuggestions() {
+        setResultsVisibility(false);
+        mQuery = EMPTY_QUERY;
+        updateSuggestions();
+    }
+
+    private void clearSuggestions() {
+        if (mUpdateSuggestionsTask != null) {
+            mUpdateSuggestionsTask.cancel(false);
+            mUpdateSuggestionsTask = null;
+        }
+        setSuggestionsCursor(null);
+    }
+
+    private void setSuggestionsCursor(Cursor cursor) {
+        if (mSuggestionsAdapter == null) {
+            return;
+        }
+        Cursor oldCursor = mSuggestionsAdapter.swapCursor(cursor);
+        if (oldCursor != null) {
+            oldCursor.close();
+        }
     }
 
     private void clearResults() {
@@ -226,20 +279,23 @@
             mUpdateSearchResultsTask.cancel(false);
             mUpdateSearchResultsTask = null;
         }
-        setCursor(null);
+        setResultsCursor(null);
     }
 
-    private void setCursor(Cursor cursor) {
-        if (mAdapter == null) {
+    private void setResultsCursor(Cursor cursor) {
+        if (mResultsAdapter == null) {
             return;
         }
-        Cursor oldCursor = mAdapter.swapCursor(cursor);
+        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);
@@ -251,20 +307,123 @@
         return filtered.toString();
     }
 
-    private void updateSearchResults(CharSequence cs) {
+    private void updateSuggestions() {
+        if (mUpdateSuggestionsTask != null) {
+            mUpdateSuggestionsTask.cancel(false);
+            mUpdateSuggestionsTask = null;
+        }
+        if (mQuery == null) {
+            setSuggestionsCursor(null);
+        } else {
+            setSuggestionsVisibility(true);
+            mUpdateSuggestionsTask = new UpdateSuggestionsTask();
+            mUpdateSuggestionsTask.execute(mQuery);
+        }
+    }
+
+    private void updateSearchResults() {
         if (mUpdateSearchResultsTask != null) {
             mUpdateSearchResultsTask.cancel(false);
             mUpdateSearchResultsTask = null;
         }
-        mQuery = getFilteredQueryString(cs);
         if (TextUtils.isEmpty(mQuery)) {
-            setCursor(null);
+            setResultsVisibility(false);
+            setResultsCursor(null);
         } else {
+            setResultsVisibility(true);
             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;
@@ -288,10 +447,10 @@
 
     private static class SearchResultsAdapter extends BaseAdapter {
 
+        private Context mContext;
         private Cursor mCursor;
         private LayoutInflater mInflater;
         private boolean mDataValid;
-        private Context mContext;
         private HashMap<String, Context> mContextMap = new HashMap<String, Context>();
 
         private static final String PERCENT_RECLACE = "%s";
@@ -299,7 +458,7 @@
 
         public SearchResultsAdapter(Context context) {
             mContext = context;
-            mInflater = (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+            mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
             mDataValid = false;
         }
 
diff --git a/src/com/android/settings/search/Index.java b/src/com/android/settings/search/Index.java
index 4f5aa2c..60660c1 100644
--- a/src/com/android/settings/search/Index.java
+++ b/src/com/android/settings/search/Index.java
@@ -47,6 +47,7 @@
 import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -131,6 +132,11 @@
             IndexColumns.DATA_KEYWORDS
     };
 
+    // Max number of saved search queries (who will be used for proposing suggestions)
+    private static long MAX_SAVED_SEARCH_QUERY = 64;
+    // Max number of proposed suggestions
+    private static final int MAX_PROPOSED_SUGGESTIONS = 5;
+
     private static final String EMPTY = "";
     private static final String NON_BREAKING_HYPHEN = "\u2011";
     private static final String HYPHEN = "-";
@@ -144,6 +150,7 @@
 
     private static final List<String> EMPTY_LIST = Collections.<String>emptyList();
 
+
     private static Index sInstance;
     private final AtomicBoolean mIsAvailable = new AtomicBoolean(false);
     private final UpdateData mDataToProcess = new UpdateData();
@@ -198,11 +205,57 @@
     }
 
     public Cursor search(String query) {
-        final String sql = buildSQL(query);
-        Log.d(LOG_TAG, "Query: " + sql);
+        final String sql = buildSearchSQL(query);
+        Log.d(LOG_TAG, "Search query: " + sql);
         return getReadableDatabase().rawQuery(sql, null);
     }
 
+    public Cursor getSuggestions(String query) {
+        final String sql = buildSuggestionsSQL(query);
+        Log.d(LOG_TAG, "Suggestions query: " + sql);
+        return getReadableDatabase().rawQuery(sql, null);
+    }
+
+    private String buildSuggestionsSQL(String query) {
+        StringBuilder sb = new StringBuilder();
+
+        sb.append("SELECT ");
+        sb.append(IndexDatabaseHelper.SavedQueriesColums.QUERY);
+        sb.append(" FROM ");
+        sb.append(Tables.TABLE_SAVED_QUERIES);
+
+        if (TextUtils.isEmpty(query)) {
+            sb.append(" ORDER BY rowId DESC");
+        } else {
+            sb.append(" WHERE ");
+            sb.append(IndexDatabaseHelper.SavedQueriesColums.QUERY);
+            sb.append(" LIKE ");
+            sb.append("'");
+            sb.append(query);
+            sb.append("%");
+            sb.append("'");
+        }
+
+        sb.append(" LIMIT ");
+        sb.append(MAX_PROPOSED_SUGGESTIONS);
+
+        return sb.toString();
+    }
+
+    public long addSavedQuery(String query){
+        final SaveSearchQueryTask task = new SaveSearchQueryTask();
+        task.execute(query);
+        try {
+            return task.get();
+        } catch (InterruptedException e) {
+            Log.e(LOG_TAG, "Cannot insert saved query: " + query, e);
+            return -1 ;
+        } catch (ExecutionException e) {
+            Log.e(LOG_TAG, "Cannot insert saved query: " + query, e);
+            return -1;
+        }
+    }
+
     public boolean update() {
         final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);
         List<ResolveInfo> list =
@@ -432,10 +485,10 @@
                 mDataToProcess.clear();
                 return result;
             } catch (InterruptedException e) {
-                Log.e(LOG_TAG, "Cannot update index: " + e.getMessage());
+                Log.e(LOG_TAG, "Cannot update index", e);
                 return false;
             } catch (ExecutionException e) {
-                Log.e(LOG_TAG, "Cannot update index: " + e.getMessage());
+                Log.e(LOG_TAG, "Cannot update index", e);
                 return false;
             }
         }
@@ -545,15 +598,15 @@
         }
     }
 
-    private String buildSQL(String query) {
+    private String buildSearchSQL(String query) {
         StringBuilder sb = new StringBuilder();
-        sb.append(buildSQLForColumn(query, MATCH_COLUMNS));
+        sb.append(buildSearchSQLForColumn(query, MATCH_COLUMNS));
         sb.append(" ORDER BY ");
         sb.append(IndexColumns.DATA_RANK);
         return sb.toString();
     }
 
-    private String buildSQLForColumn(String query, String[] columnNames) {
+    private String buildSearchSQLForColumn(String query, String[] columnNames) {
         StringBuilder sb = new StringBuilder();
         sb.append("SELECT ");
         for (int n = 0; n < SELECT_COLUMNS.length; n++) {
@@ -565,15 +618,16 @@
         sb.append(" FROM ");
         sb.append(Tables.TABLE_PREFS_INDEX);
         sb.append(" WHERE ");
-        sb.append(buildWhereStringForColumns(query, columnNames));
+        sb.append(buildSearchWhereStringForColumns(query, columnNames));
 
         return sb.toString();
     }
 
-    private String buildWhereStringForColumns(String query, String[] columnNames) {
+    private String buildSearchWhereStringForColumns(String query, String[] columnNames) {
         final StringBuilder sb = new StringBuilder(Tables.TABLE_PREFS_INDEX);
         sb.append(" MATCH ");
-        DatabaseUtils.appendEscapedSQLString(sb, buildMatchStringForColumns(query, columnNames));
+        DatabaseUtils.appendEscapedSQLString(sb,
+                buildSearchMatchStringForColumns(query, columnNames));
         sb.append(" AND ");
         sb.append(IndexColumns.LOCALE);
         sb.append(" = ");
@@ -584,7 +638,7 @@
         return sb.toString();
     }
 
-    private String buildMatchStringForColumns(String query, String[] columnNames) {
+    private String buildSearchMatchStringForColumns(String query, String[] columnNames) {
         final String value = query + "*";
         StringBuilder sb = new StringBuilder();
         final int count = columnNames.length;
@@ -1144,4 +1198,38 @@
             return result;
         }
     }
+
+    /**
+     * A basic AsynTask for saving a Search query into the database
+     */
+    private class SaveSearchQueryTask extends AsyncTask<String, Void, Long> {
+
+        @Override
+        protected Long doInBackground(String... params) {
+            final long now = new Date().getTime();
+
+            final ContentValues values = new ContentValues();
+            values.put(IndexDatabaseHelper.SavedQueriesColums.QUERY, params[0]);
+            values.put(IndexDatabaseHelper.SavedQueriesColums.TIME_STAMP, now);
+
+            final SQLiteDatabase database = getWritableDatabase();
+
+            long lastInsertedRowId = -1;
+            try {
+                lastInsertedRowId =
+                        database.replaceOrThrow(Tables.TABLE_SAVED_QUERIES, null, values);
+
+                final long delta = lastInsertedRowId - MAX_SAVED_SEARCH_QUERY;
+                if (delta > 0) {
+                    int count = database.delete(Tables.TABLE_SAVED_QUERIES, "rowId <= ?",
+                            new String[] { Long.toString(delta) });
+                    Log.d(LOG_TAG, "Deleted '" + count + "' saved Search query(ies)");
+                }
+            } catch (Exception e) {
+                Log.d(LOG_TAG, "Cannot update saved Search queries", e);
+            }
+
+            return lastInsertedRowId;
+        }
+    }
 }