Merge "Showing recents in search"
diff --git a/res/layout/search_saved_query_item.xml b/res/layout/search_saved_query_item.xml
new file mode 100644
index 0000000..71c8482
--- /dev/null
+++ b/res/layout/search_saved_query_item.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="horizontal"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?android:attr/selectableItemBackground"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:gravity="center_vertical"
+    android:paddingStart="@dimen/preference_no_icon_padding_start"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+    <TextView
+        android:id="@android:id/title"
+        android:textAppearance="?android:attr/textAppearanceListItem"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:layout_weight="1"/>
+
+    <ImageView
+        android:id="@android:id/icon"
+        android:layout_width="@dimen/search_suggestion_item_image_size"
+        android:layout_height="@dimen/search_suggestion_item_image_size"
+        android:layout_marginStart="@dimen/search_suggestion_item_image_margin_start"
+        android:layout_marginEnd="@dimen/search_suggestion_item_image_margin_end"
+        android:scaleType="centerInside"
+        android:src="@drawable/ic_search_history"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/src/com/android/settings/search/Index.java b/src/com/android/settings/search/Index.java
index cd6e562..b79a7f4 100644
--- a/src/com/android/settings/search/Index.java
+++ b/src/com/android/settings/search/Index.java
@@ -266,7 +266,7 @@
         StringBuilder sb = new StringBuilder();
 
         sb.append("SELECT ");
-        sb.append(IndexDatabaseHelper.SavedQueriesColums.QUERY);
+        sb.append(IndexDatabaseHelper.SavedQueriesColumns.QUERY);
         sb.append(" FROM ");
         sb.append(Tables.TABLE_SAVED_QUERIES);
 
@@ -274,7 +274,7 @@
             sb.append(" ORDER BY rowId DESC");
         } else {
             sb.append(" WHERE ");
-            sb.append(IndexDatabaseHelper.SavedQueriesColums.QUERY);
+            sb.append(IndexDatabaseHelper.SavedQueriesColumns.QUERY);
             sb.append(" LIKE ");
             sb.append("'");
             sb.append(query);
@@ -1299,8 +1299,8 @@
             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);
+            values.put(IndexDatabaseHelper.SavedQueriesColumns.QUERY, params[0]);
+            values.put(IndexDatabaseHelper.SavedQueriesColumns.TIME_STAMP, now);
 
             final SQLiteDatabase database = getWritableDatabase();
             if (database == null) {
@@ -1312,7 +1312,7 @@
             try {
                 // First, delete all saved queries that are the same
                 database.delete(Tables.TABLE_SAVED_QUERIES,
-                        IndexDatabaseHelper.SavedQueriesColums.QUERY + " = ?",
+                        IndexDatabaseHelper.SavedQueriesColumns.QUERY + " = ?",
                         new String[] { params[0] });
 
                 // Second, insert the saved query
diff --git a/src/com/android/settings/search/IndexDatabaseHelper.java b/src/com/android/settings/search/IndexDatabaseHelper.java
index ba53e94..8de6c54 100644
--- a/src/com/android/settings/search/IndexDatabaseHelper.java
+++ b/src/com/android/settings/search/IndexDatabaseHelper.java
@@ -67,7 +67,7 @@
         String BUILD = "build";
     }
 
-    public interface SavedQueriesColums {
+    public interface SavedQueriesColumns {
         String QUERY = "query";
         String TIME_STAMP = "timestamp";
     }
@@ -127,9 +127,9 @@
     private static final String CREATE_SAVED_QUERIES_TABLE =
             "CREATE TABLE " + Tables.TABLE_SAVED_QUERIES +
                     "(" +
-                    SavedQueriesColums.QUERY + " VARCHAR(64) NOT NULL" +
+                    SavedQueriesColumns.QUERY + " VARCHAR(64) NOT NULL" +
                     ", " +
-                    SavedQueriesColums.TIME_STAMP + " INTEGER" +
+                    SavedQueriesColumns.TIME_STAMP + " INTEGER" +
                     ")";
 
     private static final String INSERT_BUILD_VERSION =
diff --git a/src/com/android/settings/search2/ResultPayload.java b/src/com/android/settings/search2/ResultPayload.java
index 3842def..4294234 100644
--- a/src/com/android/settings/search2/ResultPayload.java
+++ b/src/com/android/settings/search2/ResultPayload.java
@@ -29,7 +29,7 @@
 public abstract class ResultPayload implements Parcelable {
 
     @IntDef({PayloadType.INLINE_SLIDER, PayloadType.INLINE_SWITCH,
-            PayloadType.INTENT})
+            PayloadType.INTENT, PayloadType.SAVED_QUERY})
     @Retention(RetentionPolicy.SOURCE)
     public @interface PayloadType {
         /**
@@ -46,6 +46,11 @@
          * Result is a inline widget, using a toggle widget as UI.
          */
         int INLINE_SWITCH = 2;
+
+        /**
+         * Result is a recently saved query.
+         */
+        int SAVED_QUERY = 3;
     }
 
     @IntDef({SettingsSource.UNKNOWN, SettingsSource.SYSTEM, SettingsSource.SECURE,
@@ -59,5 +64,6 @@
     }
 
 
-    @ResultPayload.PayloadType public abstract int getType();
+    @ResultPayload.PayloadType
+    public abstract int getType();
 }
diff --git a/src/com/android/settings/search2/SavedQueryLoader.java b/src/com/android/settings/search2/SavedQueryLoader.java
new file mode 100644
index 0000000..b034b44
--- /dev/null
+++ b/src/com/android/settings/search2/SavedQueryLoader.java
@@ -0,0 +1,77 @@
+/*
+ * 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.search2;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.support.annotation.VisibleForTesting;
+
+import com.android.settings.search.IndexDatabaseHelper;
+import com.android.settings.search.IndexDatabaseHelper.SavedQueriesColumns;
+import com.android.settings.utils.AsyncLoader;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Loader for recently searched queries.
+ */
+public class SavedQueryLoader extends AsyncLoader<List<SearchResult>> {
+
+    // Max number of proposed suggestions
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    static final int MAX_PROPOSED_SUGGESTIONS = 5;
+
+    private final SQLiteDatabase mDatabase;
+
+    public SavedQueryLoader(Context context) {
+        super(context);
+        mDatabase = IndexDatabaseHelper.getInstance(context).getReadableDatabase();
+    }
+
+    @Override
+    protected void onDiscardResult(List<SearchResult> result) {
+
+    }
+
+    @Override
+    public List<SearchResult> loadInBackground() {
+        Cursor cursor = mDatabase.query(IndexDatabaseHelper.Tables.TABLE_SAVED_QUERIES /* table */,
+                new String[]{SavedQueriesColumns.QUERY} /* columns */,
+                null /* selection */,
+                null /* selectionArgs */,
+                null /* groupBy */,
+                null /* having */,
+                "rowId DESC" /* orderBy */,
+                String.valueOf(MAX_PROPOSED_SUGGESTIONS) /* limit */);
+        return convertCursorToResult(cursor);
+    }
+
+    private List<SearchResult> convertCursorToResult(Cursor cursor) {
+        final List<SearchResult> results = new ArrayList<>();
+        while (cursor.moveToNext()) {
+            final SavedQueryPayload payload = new SavedQueryPayload(
+                    cursor.getString(cursor.getColumnIndex(SavedQueriesColumns.QUERY)));
+            results.add(new SearchResult.Builder()
+                    .addTitle(payload.query)
+                    .addPayload(payload)
+                    .build());
+        }
+        return results;
+    }
+}
diff --git a/src/com/android/settings/search2/SavedQueryPayload.java b/src/com/android/settings/search2/SavedQueryPayload.java
new file mode 100644
index 0000000..6316894
--- /dev/null
+++ b/src/com/android/settings/search2/SavedQueryPayload.java
@@ -0,0 +1,64 @@
+/*
+ * 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.search2;
+
+import android.os.Parcel;
+import android.support.annotation.VisibleForTesting;
+
+/**
+ * {@link ResultPayload} for saved query.
+ */
+public class SavedQueryPayload extends ResultPayload {
+
+    public final String query;
+
+    public SavedQueryPayload(String query) {
+        this.query = query;
+    }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    SavedQueryPayload(Parcel in) {
+        query = in.readString();
+    }
+
+    @Override
+    public int getType() {
+        return PayloadType.SAVED_QUERY;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(query);
+    }
+
+    public static final Creator<SavedQueryPayload> CREATOR = new Creator<SavedQueryPayload>() {
+        @Override
+        public SavedQueryPayload createFromParcel(Parcel in) {
+            return new SavedQueryPayload(in);
+        }
+
+        @Override
+        public SavedQueryPayload[] newArray(int size) {
+            return new SavedQueryPayload[size];
+        }
+    };
+}
diff --git a/src/com/android/settings/search2/SavedQueryRecorder.java b/src/com/android/settings/search2/SavedQueryRecorder.java
new file mode 100644
index 0000000..e2325e8
--- /dev/null
+++ b/src/com/android/settings/search2/SavedQueryRecorder.java
@@ -0,0 +1,97 @@
+/*
+ * 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.search2;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.util.Log;
+
+import com.android.settings.search.IndexDatabaseHelper;
+import com.android.settings.utils.AsyncLoader;
+
+import static com.android.settings.search.IndexDatabaseHelper.Tables.TABLE_SAVED_QUERIES;
+
+/**
+ * A background task to update saved queries.
+ */
+public class SavedQueryRecorder extends AsyncLoader<Void> {
+
+    private static final String LOG_TAG = "SavedQueryRecorder";
+
+    // Max number of saved search queries (who will be used for proposing suggestions)
+    private static long MAX_SAVED_SEARCH_QUERY = 64;
+
+    private final String mQuery;
+
+    public SavedQueryRecorder(Context context, String query) {
+        super(context);
+        mQuery = query;
+    }
+
+    @Override
+    protected void onDiscardResult(Void result) {
+
+    }
+
+    @Override
+    public Void loadInBackground() {
+        final long now = System.currentTimeMillis();
+
+        final ContentValues values = new ContentValues();
+        values.put(IndexDatabaseHelper.SavedQueriesColumns.QUERY, mQuery);
+        values.put(IndexDatabaseHelper.SavedQueriesColumns.TIME_STAMP, now);
+
+        final SQLiteDatabase database = getWritableDatabase();
+        if (database == null) {
+            return null;
+        }
+
+        long lastInsertedRowId;
+        try {
+            // First, delete all saved queries that are the same
+            database.delete(TABLE_SAVED_QUERIES,
+                    IndexDatabaseHelper.SavedQueriesColumns.QUERY + " = ?",
+                    new String[]{mQuery});
+
+            // Second, insert the saved query
+            lastInsertedRowId = database.insertOrThrow(TABLE_SAVED_QUERIES, null, values);
+
+            // Last, remove "old" saved queries
+            final long delta = lastInsertedRowId - MAX_SAVED_SEARCH_QUERY;
+            if (delta > 0) {
+                int count = database.delete(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 null;
+    }
+
+    private SQLiteDatabase getWritableDatabase() {
+        try {
+            return IndexDatabaseHelper.getInstance(getContext()).getWritableDatabase();
+        } catch (SQLiteException e) {
+            Log.e(LOG_TAG, "Cannot open writable database", e);
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/settings/search2/SavedQueryViewHolder.java b/src/com/android/settings/search2/SavedQueryViewHolder.java
new file mode 100644
index 0000000..a32ed05
--- /dev/null
+++ b/src/com/android/settings/search2/SavedQueryViewHolder.java
@@ -0,0 +1,38 @@
+/*
+ * 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.search2;
+
+import android.view.View;
+import android.widget.TextView;
+
+public class SavedQueryViewHolder extends SearchViewHolder {
+
+    public final TextView titleView;
+
+    public SavedQueryViewHolder(View view) {
+        super(view);
+        titleView = (TextView) view.findViewById(android.R.id.title);
+    }
+
+    @Override
+    public void onBind(SearchFragment fragment, SearchResult result) {
+        titleView.setText(result.title);
+        itemView.setOnClickListener(v -> {
+            fragment.onSavedQueryClicked(result.title);
+        });
+    }
+}
diff --git a/src/com/android/settings/search2/SearchFeatureProvider.java b/src/com/android/settings/search2/SearchFeatureProvider.java
index 8a616a7..91a1444 100644
--- a/src/com/android/settings/search2/SearchFeatureProvider.java
+++ b/src/com/android/settings/search2/SearchFeatureProvider.java
@@ -48,6 +48,11 @@
     InstalledAppResultLoader getInstalledAppSearchLoader(Context context, String query);
 
     /**
+     * Returns a new loader to get all recently saved queries search terms.
+     */
+    SavedQueryLoader getSavedQueryLoader(Context context);
+
+    /**
      * Returns the manager for indexing Settings data.
      */
     DatabaseIndexingManager getIndexingManager(Context context);
diff --git a/src/com/android/settings/search2/SearchFeatureProviderImpl.java b/src/com/android/settings/search2/SearchFeatureProviderImpl.java
index a76d905..5d62412 100644
--- a/src/com/android/settings/search2/SearchFeatureProviderImpl.java
+++ b/src/com/android/settings/search2/SearchFeatureProviderImpl.java
@@ -73,6 +73,11 @@
     }
 
     @Override
+    public SavedQueryLoader getSavedQueryLoader(Context context) {
+        return new SavedQueryLoader(context);
+    }
+
+    @Override
     public DatabaseIndexingManager getIndexingManager(Context context) {
         if (mDatabaseIndexingManager == null) {
             mDatabaseIndexingManager = new DatabaseIndexingManager(context.getApplicationContext(),
diff --git a/src/com/android/settings/search2/SearchFragment.java b/src/com/android/settings/search2/SearchFragment.java
index e26f5ed..b688a45 100644
--- a/src/com/android/settings/search2/SearchFragment.java
+++ b/src/com/android/settings/search2/SearchFragment.java
@@ -52,8 +52,9 @@
     private static final String STATE_RESULT_CLICK_COUNT = "state_result_click_count";
 
     // Loader IDs
-    private static final int LOADER_ID_DATABASE = 0;
-    private static final int LOADER_ID_INSTALLED_APPS = 1;
+    private static final int LOADER_ID_RECENTS = 0;
+    private static final int LOADER_ID_DATABASE = 1;
+    private static final int LOADER_ID_INSTALLED_APPS = 2;
 
     // Logging
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@@ -61,6 +62,10 @@
 
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
     String mQuery;
+
+    private final SaveQueryRecorderCallback mSaveQueryRecorderCallback =
+            new SaveQueryRecorderCallback();
+
     private boolean mNeverEnteredQuery = true;
     private int mResultClickCount;
     private MetricsFeatureProvider mMetricsFeatureProvider;
@@ -68,6 +73,7 @@
 
     private SearchResultsAdapter mSearchAdapter;
     private RecyclerView mResultsRecyclerView;
+    private SearchView mSearchView;
 
     @Override
     public int getMetricsCategory() {
@@ -86,18 +92,21 @@
         super.onCreate(savedInstanceState);
         setHasOptionsMenu(true);
         mSearchAdapter = new SearchResultsAdapter(this);
+        final LoaderManager loaderManager = getLoaderManager();
         if (savedInstanceState != null) {
             mQuery = savedInstanceState.getString(STATE_QUERY);
             mNeverEnteredQuery = savedInstanceState.getBoolean(STATE_NEVER_ENTERED_QUERY);
             mResultClickCount = savedInstanceState.getInt(STATE_RESULT_CLICK_COUNT);
-            final LoaderManager loaderManager = getLoaderManager();
             loaderManager.initLoader(LOADER_ID_DATABASE, null, this);
             loaderManager.initLoader(LOADER_ID_INSTALLED_APPS, null, this);
+        } else {
+            loaderManager.initLoader(LOADER_ID_RECENTS, null, this);
         }
 
         final Activity activity = getActivity();
         final ActionBar actionBar = activity.getActionBar();
-        actionBar.setCustomView(makeSearchView(actionBar, mQuery));
+        mSearchView = makeSearchView(actionBar, mQuery);
+        actionBar.setCustomView(mSearchView);
         actionBar.setDisplayShowCustomEnabled(true);
         actionBar.setDisplayShowTitleEnabled(false);
 
@@ -151,7 +160,10 @@
         mSearchAdapter.clearResults();
 
         if (TextUtils.isEmpty(mQuery)) {
-            getLoaderManager().destroyLoader(LOADER_ID_DATABASE);
+            final LoaderManager loaderManager = getLoaderManager();
+            loaderManager.destroyLoader(LOADER_ID_DATABASE);
+            loaderManager.destroyLoader(LOADER_ID_INSTALLED_APPS);
+            loaderManager.restartLoader(LOADER_ID_RECENTS, null /* args */, this /* callback */);
         } else {
             restartLoaders();
         }
@@ -161,6 +173,10 @@
 
     @Override
     public boolean onQueryTextSubmit(String query) {
+        // Save submitted query.
+        getLoaderManager().restartLoader(SaveQueryRecorderCallback.LOADER_ID_SAVE_QUERY_TASK, null,
+                mSaveQueryRecorderCallback);
+
         return true;
     }
 
@@ -173,6 +189,8 @@
                 return mSearchFeatureProvider.getDatabaseSearchLoader(activity, mQuery);
             case LOADER_ID_INSTALLED_APPS:
                 return mSearchFeatureProvider.getInstalledAppSearchLoader(activity, mQuery);
+            case LOADER_ID_RECENTS:
+                return mSearchFeatureProvider.getSavedQueryLoader(activity);
             default:
                 return null;
         }
@@ -191,6 +209,12 @@
         mResultClickCount++;
     }
 
+    public void onSavedQueryClicked(CharSequence query) {
+        final String queryString = query.toString();
+        mSearchView.setQuery(queryString, false /* submit */);
+        onQueryTextChange(queryString);
+    }
+
     private void restartLoaders() {
         final LoaderManager loaderManager = getLoaderManager();
         loaderManager.restartLoader(LOADER_ID_DATABASE, null /* args */, this /* callback */);
@@ -207,4 +231,25 @@
         searchView.setLayoutParams(lp);
         return searchView;
     }
+
+    private class SaveQueryRecorderCallback implements LoaderManager.LoaderCallbacks<Void> {
+        // TODO: make a generic background task manager to handle one-off tasks like this one.
+
+        private static final int LOADER_ID_SAVE_QUERY_TASK = 0;
+
+        @Override
+        public Loader<Void> onCreateLoader(int id, Bundle args) {
+            return new SavedQueryRecorder(getActivity(), mQuery);
+        }
+
+        @Override
+        public void onLoadFinished(Loader<Void> loader, Void data) {
+
+        }
+
+        @Override
+        public void onLoaderReset(Loader<Void> loader) {
+
+        }
+    }
 }
diff --git a/src/com/android/settings/search2/SearchResultsAdapter.java b/src/com/android/settings/search2/SearchResultsAdapter.java
index c318b41..999a485 100644
--- a/src/com/android/settings/search2/SearchResultsAdapter.java
+++ b/src/com/android/settings/search2/SearchResultsAdapter.java
@@ -49,13 +49,16 @@
         final Context context = parent.getContext();
         final LayoutInflater inflater = LayoutInflater.from(context);
         final View view;
-        switch(viewType) {
+        switch (viewType) {
             case PayloadType.INTENT:
                 view = inflater.inflate(R.layout.search_intent_item, parent, false);
                 return new IntentSearchViewHolder(view);
             case PayloadType.INLINE_SWITCH:
                 view = inflater.inflate(R.layout.search_inline_switch_item, parent, false);
                 return new InlineSwitchViewHolder(view, context);
+            case PayloadType.SAVED_QUERY:
+                view = inflater.inflate(R.layout.search_saved_query_item, parent, false);
+                return new SavedQueryViewHolder(view);
             default:
                 return null;
         }
diff --git a/tests/robotests/assets/grandfather_not_implementing_indexable b/tests/robotests/assets/grandfather_not_implementing_indexable
index cd0822b..a178596 100644
--- a/tests/robotests/assets/grandfather_not_implementing_indexable
+++ b/tests/robotests/assets/grandfather_not_implementing_indexable
@@ -90,4 +90,4 @@
 com.android.settings.applications.ConvertToFbe
 com.android.settings.localepicker.LocaleListEditor
 com.android.settings.qstile.DevelopmentTileConfigActivity$DevelopmentTileConfigFragment
-com.android.settings.applications.ExternalSourcesDetails
\ No newline at end of file
+com.android.settings.applications.ExternalSourcesDetails
diff --git a/tests/robotests/src/com/android/settings/search/DatabaseIndexingManagerTest.java b/tests/robotests/src/com/android/settings/search/DatabaseIndexingManagerTest.java
index 8b363b0..3d469dd 100644
--- a/tests/robotests/src/com/android/settings/search/DatabaseIndexingManagerTest.java
+++ b/tests/robotests/src/com/android/settings/search/DatabaseIndexingManagerTest.java
@@ -21,10 +21,12 @@
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.provider.SearchIndexableResource;
+
 import com.android.settings.R;
 import com.android.settings.SettingsRobolectricTestRunner;
 import com.android.settings.TestConfig;
 import com.android.settings.search2.DatabaseIndexingManager;
+import com.android.settings.testutils.DatabaseTestUtils;
 
 import org.junit.After;
 import org.junit.Before;
@@ -33,7 +35,6 @@
 import org.robolectric.annotation.Config;
 import org.robolectric.shadows.ShadowApplication;
 
-import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -85,15 +86,7 @@
 
     @After
     public void cleanUp() {
-        Field instance;
-        Class clazz = IndexDatabaseHelper.class;
-        try {
-            instance = clazz.getDeclaredField("sSingleton");
-            instance.setAccessible(true);
-            instance.set(null, null);
-        } catch (Exception e) {
-            throw new RuntimeException();
-        }
+        DatabaseTestUtils.clearDb();
     }
 
     @Test
diff --git a/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java b/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java
index c592aef..2b29a16 100644
--- a/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java
+++ b/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java
@@ -20,10 +20,13 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.sqlite.SQLiteDatabase;
+
 import com.android.settings.SettingsRobolectricTestRunner;
 import com.android.settings.TestConfig;
 import com.android.settings.search2.DatabaseIndexingUtils;
 import com.android.settings.search2.DatabaseResultLoader;
+import com.android.settings.testutils.DatabaseTestUtils;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -31,8 +34,6 @@
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
 
-import java.lang.reflect.Field;
-
 import static com.google.common.truth.Truth.assertThat;
 
 @RunWith(SettingsRobolectricTestRunner.class)
@@ -53,15 +54,7 @@
 
     @After
     public void cleanUp() {
-        Field instance;
-        Class clazz = IndexDatabaseHelper.class;
-        try {
-            instance = clazz.getDeclaredField("sSingleton");
-            instance.setAccessible(true);
-            instance.set(null, null);
-        } catch (Exception e) {
-            throw new RuntimeException();
-        }
+        DatabaseTestUtils.clearDb();
     }
 
     @Test
diff --git a/tests/robotests/src/com/android/settings/search2/SavedQueryLoaderTest.java b/tests/robotests/src/com/android/settings/search2/SavedQueryLoaderTest.java
new file mode 100644
index 0000000..d975f0c
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/search2/SavedQueryLoaderTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.search2;
+
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+
+import com.android.settings.SettingsRobolectricTestRunner;
+import com.android.settings.TestConfig;
+import com.android.settings.search.IndexDatabaseHelper;
+import com.android.settings.testutils.DatabaseTestUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.util.List;
+
+import static com.google.common.truth.Truth.assertThat;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class SavedQueryLoaderTest {
+
+    private Context mContext;
+    private SQLiteDatabase mDb;
+    private SavedQueryLoader mLoader;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+        mDb = IndexDatabaseHelper.getInstance(mContext).getWritableDatabase();
+        mLoader = new SavedQueryLoader(mContext);
+        setUpDb();
+    }
+
+    @After
+    public void cleanUp() {
+        DatabaseTestUtils.clearDb();
+    }
+
+    @Test
+    public void loadInBackground_shouldReturnSavedQueries() {
+        final List<SearchResult> results = mLoader.loadInBackground();
+        assertThat(results.size()).isEqualTo(SavedQueryLoader.MAX_PROPOSED_SUGGESTIONS);
+        for (SearchResult result : results) {
+            assertThat(result.viewType).isEqualTo(ResultPayload.PayloadType.SAVED_QUERY);
+        }
+    }
+
+    private void setUpDb() {
+        final long now = System.currentTimeMillis();
+        for (int i = 0; i < SavedQueryLoader.MAX_PROPOSED_SUGGESTIONS + 2; i++) {
+            ContentValues values = new ContentValues();
+            values.put(IndexDatabaseHelper.SavedQueriesColumns.QUERY, String.valueOf(i));
+            values.put(IndexDatabaseHelper.SavedQueriesColumns.TIME_STAMP, now);
+            mDb.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_SAVED_QUERIES, null, values);
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/search2/SavedQueryPayloadTest.java b/tests/robotests/src/com/android/settings/search2/SavedQueryPayloadTest.java
new file mode 100644
index 0000000..daa6d5e
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/search2/SavedQueryPayloadTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.search2;
+
+
+import com.android.settings.SettingsRobolectricTestRunner;
+import com.android.settings.TestConfig;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class SavedQueryPayloadTest {
+
+    private SavedQueryPayload mPayload;
+
+    @Test
+    public void getType_shouldBeSavedQueryType() {
+        mPayload = new SavedQueryPayload("Test");
+        assertThat(mPayload.getType()).isEqualTo(ResultPayload.PayloadType.SAVED_QUERY);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/search2/SavedQueryRecorderTest.java b/tests/robotests/src/com/android/settings/search2/SavedQueryRecorderTest.java
new file mode 100644
index 0000000..c56ecce
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/search2/SavedQueryRecorderTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.search2;
+
+
+import android.content.Context;
+
+import com.android.settings.SettingsRobolectricTestRunner;
+import com.android.settings.TestConfig;
+import com.android.settings.testutils.DatabaseTestUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.util.List;
+
+import static com.google.common.truth.Truth.assertThat;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class SavedQueryRecorderTest {
+
+    private Context mContext;
+    private SavedQueryRecorder mRecorder;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+    }
+
+    @After
+    public void cleanUp() {
+        DatabaseTestUtils.clearDb();
+    }
+
+    @Test
+    public void canSaveQueryToDb() {
+        final String query = "test";
+        mRecorder = new SavedQueryRecorder(mContext, query);
+
+        mRecorder.loadInBackground();
+
+        final SavedQueryLoader loader = new SavedQueryLoader(mContext);
+        List<SearchResult> results = loader.loadInBackground();
+
+        assertThat(results.size()).isEqualTo(1);
+        assertThat(results.get(0).title).isEqualTo(query);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java b/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java
index d97360d..7a0bb54 100644
--- a/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java
@@ -39,6 +39,7 @@
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -52,6 +53,9 @@
     private DatabaseResultLoader mDatabaseResultLoader;
     @Mock
     private InstalledAppResultLoader mInstalledAppResultLoader;
+    @Mock
+    private SavedQueryLoader mSavedQueryLoader;
+
     private FakeFeatureFactory mFeatureFactory;
 
     @Before
@@ -65,6 +69,8 @@
         when(mFeatureFactory.searchFeatureProvider
                 .getInstalledAppSearchLoader(any(Context.class), anyString()))
                 .thenReturn(mInstalledAppResultLoader);
+        when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
+                .thenReturn(mSavedQueryLoader);
     }
 
     @Test
@@ -115,6 +121,27 @@
     }
 
     @Test
+    public void queryTextChangeToEmpty_shouldTriggerSavedQueryLoader() {
+        ActivityController<SearchActivity> activityController =
+                Robolectric.buildActivity(SearchActivity.class);
+        activityController.setup();
+        SearchFragment fragment = (SearchFragment) activityController.get().getFragmentManager()
+                .findFragmentById(R.id.main_content);
+
+        fragment.onQueryTextChange("");
+        activityController.get().onBackPressed();
+        activityController.pause().stop().destroy();
+
+        verify(mFeatureFactory.searchFeatureProvider, never())
+                .getDatabaseSearchLoader(any(Context.class), anyString());
+        verify(mFeatureFactory.searchFeatureProvider, never())
+                .getInstalledAppSearchLoader(any(Context.class), anyString());
+        // Saved query loaded 2 times: fragment start, and query change to empty.
+        verify(mFeatureFactory.searchFeatureProvider, times(2))
+                .getSavedQueryLoader(any(Context.class));
+    }
+
+    @Test
     public void updateIndex_TriggerOnCreate() {
         ActivityController<SearchActivity> activityController =
                 Robolectric.buildActivity(SearchActivity.class);
diff --git a/tests/robotests/src/com/android/settings/testutils/DatabaseTestUtils.java b/tests/robotests/src/com/android/settings/testutils/DatabaseTestUtils.java
new file mode 100644
index 0000000..8fbe1c9
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/testutils/DatabaseTestUtils.java
@@ -0,0 +1,36 @@
+/*
+ * 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.testutils;
+
+import com.android.settings.search.IndexDatabaseHelper;
+
+import java.lang.reflect.Field;
+
+public class DatabaseTestUtils {
+
+    public static void clearDb() {
+        Field instance;
+        Class clazz = IndexDatabaseHelper.class;
+        try {
+            instance = clazz.getDeclaredField("sSingleton");
+            instance.setAccessible(true);
+            instance.set(null, null);
+        } catch (Exception e) {
+            throw new RuntimeException();
+        }
+    }
+}