DO NOT MERGE Snap to latest SettingsIntelligence source

Bug: 67755462
Test: rebuild
Change-Id: Ib606a68c10f83c3a1479f234af19fe840b3120d0
diff --git a/Android.mk b/Android.mk
index 1efed08..b565774 100644
--- a/Android.mk
+++ b/Android.mk
@@ -24,14 +24,23 @@
 LOCAL_PRIVILEGED_MODULE := true
 
 LOCAL_STATIC_ANDROID_LIBRARIES := \
-    android-support-v4
+    android-support-v4 \
+    android-support-v13 \
+    android-support-v7-appcompat \
+    android-support-v7-cardview \
+    android-support-v7-preference \
+    android-support-v7-recyclerview \
+    android-support-v14-preference
 
 LOCAL_USE_AAPT2 := true
 
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
 
 LOCAL_SRC_FILES := \
-    $(call all-java-files-under, src)
+    $(call all-java-files-under, src) \
+    $(call all-proto-files-under, proto)
+
+LOCAL_PROTOC_OPTIMIZE_TYPE := nano
 
 include $(BUILD_PACKAGE)
 
diff --git a/proto/settings_intelligence_log.proto b/proto/settings_intelligence_log.proto
new file mode 100644
index 0000000..9bfd28f
--- /dev/null
+++ b/proto/settings_intelligence_log.proto
@@ -0,0 +1,92 @@
+syntax = "proto2";
+
+option java_outer_classname = "SettingsIntelligenceLogProto";
+
+package com.android.settings.intelligence;
+
+// Wrapper for SettingsIntelligence event.
+// Next index: 3
+message SettingsIntelligenceEvent {
+
+    // Event type for this log.
+    enum EventType {
+        // Do not use
+        UNUSED = 0;
+
+        // Gets suggestion list
+        GET_SUGGESTION = 1;
+
+        // Dismisses a suggestion
+        DISMISS_SUGGESTION = 2;
+
+        // Launches a suggestion
+        LAUNCH_SUGGESTION = 3;
+
+        // Opens search page
+        OPEN_SEARCH_PAGE = 4;
+
+        // Leaves search page
+        LEAVE_SEARCH_PAGE = 5;
+
+        // User sends a query to settings search
+        PERFORM_SEARCH = 6;
+
+        // Clicks a search result
+        CLICK_SEARCH_RESULT = 7;
+
+        // Clicks a saved query
+        CLICK_SAVED_QUERY = 8;
+
+        // Search service indexes database
+        INDEX_SEARCH = 9;
+
+        // Displays the no result image in search
+        SHOW_SEARCH_NO_RESULT = 10;
+
+        // Displays some result in search
+        SHOW_SEARCH_RESULT = 11;
+
+        // Leaves search page without entering any query
+        LEAVE_SEARCH_WITHOUT_QUERY = 12;
+
+        // Queries search data during a search session
+        SEARCH_QUERY_DATABASE = 13;
+
+        // Queries installed app list during a search session
+        SEARCH_QUERY_INSTALLED_APPS = 14;
+
+        // Queries input device list (keyboards, game controller etc) during
+        // a search session
+        SEARCH_QUERY_INPUT_DEVICES = 15;
+
+        // Queries accessiblity service list during a search session
+        SEARCH_QUERY_ACCESSIBILITY_SERVICES = 16;
+    }
+
+    message SearchResultMetadata {
+        // The id of the search result row in this event, this is an internally
+        // generated key and does not associate with any user data.
+        optional string search_result_key = 1;
+
+        // The rank of the search result row in this event.
+        optional int32 search_result_rank = 2;
+
+        // The number of results in this query.
+        optional int32 result_count = 3;
+
+        // The length of query word.
+        optional int32 search_query_length = 4;
+    }
+
+    // The type of suggestion event.
+    optional EventType event_type = 1;
+
+    // The name/id of the suggestion in this event.
+    repeated string suggestion_ids = 2;
+
+    // Data about search results in this event.
+    optional SearchResultMetadata search_result_metadata = 3;
+
+    // Latency for the current event.
+    optional int64 latency_millis = 4;
+}
diff --git a/res/drawable/empty_search_results.xml b/res/drawable/empty_search_results.xml
new file mode 100644
index 0000000..9162107
--- /dev/null
+++ b/res/drawable/empty_search_results.xml
@@ -0,0 +1,30 @@
+<?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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="96dp"
+    android:height="96dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?android:attr/colorControlNormal">
+
+    <path
+        android:fillColor="#000000"
+        android:pathData="M15.5,14h-0.79l-0.28-0.27c1.2-1.4,1.82-3.31,1.48-5.34c-0.47-2.78-2.79-5-5.59-5.34c-4.23-0.52-7.79,3.04-7.27,7.27
+c0.34,2.8,2.56,5.12,5.34,5.59c2.03,0.34,3.94-0.28,5.34-1.48L14,14.71v0.79l5.2,5.19c0.41,0.41,1.07,0.41,1.48,0l0.01-0.01
+c0.41-0.41,0.41-1.07,0-1.48L15.5,14z M9.5,14C7.01,14,5,11.99,5,9.5S7.01,5,9.5,5S14,7.01,14,9.5S11.99,14,9.5,14z" />
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_close_24dp.xml b/res/drawable/ic_close_24dp.xml
new file mode 100644
index 0000000..17451d3
--- /dev/null
+++ b/res/drawable/ic_close_24dp.xml
@@ -0,0 +1,28 @@
+<?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.
+
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0"
+        android:tint="?android:attr/colorControlNormal">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+</vector>
diff --git a/res/drawable/ic_help_24dp.xml b/res/drawable/ic_help_24dp.xml
new file mode 100644
index 0000000..3f554e7
--- /dev/null
+++ b/res/drawable/ic_help_24dp.xml
@@ -0,0 +1,31 @@
+<?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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0"
+        android:tint="?android:attr/colorControlNormal">
+    <path
+        android:pathData="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 16.81c-.72
+0-1.3-.58-1.3-1.3s.58-1.3 1.3-1.3 1.3 .58 1.3 1.3-.58 1.3-1.3
+1.3zm1.07-4.62c-.09 .52 -.59 .87 -1.13 .79 -.57-.08-.94-.66-.83-1.23 .52 -2.61
+2.66-2.84 2.87-4.5 .12 -.96-.42-1.87-1.34-2.17-1.04-.33-2.21 .16 -2.55 1.37-.12
+.45 -.52 .74 -.97 .74 -.66 0-1.13-.63-.96-1.27 .63 -2.25 2.91-3.38 5.05-2.74
+1.71 .51 2.84 2.16 2.78 3.95-.07 2.44-2.49 2.61-2.92 5.06z"
+        android:fillColor="#FFFFFFFF"/>
+</vector>
diff --git a/res/drawable/ic_restore.xml b/res/drawable/ic_restore.xml
new file mode 100644
index 0000000..201b1df
--- /dev/null
+++ b/res/drawable/ic_restore.xml
@@ -0,0 +1,32 @@
+<?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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24.0dp"
+    android:height="24.0dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0"
+    android:tint="?android:attr/colorControlNormal">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M13.5 7.75v4.5l3.37 2c.34 .2 .46 .65 .25 .99 s-.64 .45 -.98 .24 L12
+13V7.75c0-.41 .34 -.75 .75 -.75s.75 .34 .75 .75 zM13.05 3C8.11 3 4.1 7.02 4.1
+11.95c0 .02 .01 .03 .01 .05H2.05c-.47 0-.71 .57 -.37 .9 l2.95 2.94c.21 .21 .54
+.21 .75 0l2.95-2.94c.33-.33 .1 -.9-.37-.9H5.99c0-.02 .01 -.03 .01 -.05C6 8.06
+9.16 4.9 13.05 4.9S20.1 8.11 20.1 12s-3.16 7.1-7.05 7.1c-1.58
+0-3.08-.51-4.32-1.48a.94 .94 0 0 0-1.32 .16 l-.01 .01 a.94 .94 0 0 0 .16
+1.32l.01 .01 A8.77 8.77 0 0 0 13.05 21c4.94 0 8.95-4.07 8.95-9s-4.02-9-8.95-9z" />
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_search_24dp.xml b/res/drawable/ic_search_24dp.xml
new file mode 100644
index 0000000..8f14e88
--- /dev/null
+++ b/res/drawable/ic_search_24dp.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2016 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?android:attr/colorControlNormal">
+
+    <path
+        android:fillColor="#000000"
+        android:pathData="M15.5,14h-0.79l-0.28-0.27c1.2-1.4,1.82-3.31,1.48-5.34c-0.47-2.78-2.79-5-5.59-5.34c-4.23-0.52-7.79,3.04-7.27,7.27
+c0.34,2.8,2.56,5.12,5.34,5.59c2.03,0.34,3.94-0.28,5.34-1.48L14,14.71v0.79l5.2,5.19c0.41,0.41,1.07,0.41,1.48,0l0.01-0.01
+c0.41-0.41,0.41-1.07,0-1.48L15.5,14z M9.5,14C7.01,14,5,11.99,5,9.5S7.01,5,9.5,5S14,7.01,14,9.5S11.99,14,9.5,14z" />
+</vector>
\ No newline at end of file
diff --git a/res/layout/search_breadcrumb_view.xml b/res/layout/search_breadcrumb_view.xml
new file mode 100644
index 0000000..08a2651
--- /dev/null
+++ b/res/layout/search_breadcrumb_view.xml
@@ -0,0 +1,26 @@
+<?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.
+  -->
+
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/breadcrumb"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:paddingTop="5dp"
+    android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+    android:textColor="?android:attr/textColorSecondary"
+    android:ellipsize="marquee"/>
diff --git a/res/layout/search_feedback.xml b/res/layout/search_feedback.xml
new file mode 100644
index 0000000..c9f1fc1
--- /dev/null
+++ b/res/layout/search_feedback.xml
@@ -0,0 +1,23 @@
+<?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.
+-->
+
+<View
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/feedback_popup"
+    android:layout_width="0dp"
+    android:layout_height="0dp"
+    android:visibility="gone">
+</View>
\ No newline at end of file
diff --git a/res/layout/search_icon_view.xml b/res/layout/search_icon_view.xml
new file mode 100644
index 0000000..ef99bee
--- /dev/null
+++ b/res/layout/search_icon_view.xml
@@ -0,0 +1,25 @@
+<?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.
+  -->
+
+<ImageView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/icon"
+    android:layout_width="@dimen/dashboard_tile_image_size"
+    android:layout_height="match_parent"
+    android:scaleType="centerInside"
+    android:layout_marginStart="@dimen/dashboard_tile_image_margin"
+    android:layout_marginEnd="@dimen/dashboard_tile_image_margin" />
diff --git a/res/layout/search_intent_item.xml b/res/layout/search_intent_item.xml
new file mode 100644
index 0000000..6bc00ad
--- /dev/null
+++ b/res/layout/search_intent_item.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:paddingTop="16dp"
+    android:paddingBottom="16dp"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:background="?android:attr/selectableItemBackground"
+    android:clipToPadding="false">
+
+    <include layout="@layout/search_icon_view"/>
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@android:id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            android:textAppearance="?android:attr/textAppearanceListItem"
+            android:ellipsize="marquee"/>
+
+        <TextView
+            android:id="@android:id/summary"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+            android:ellipsize="marquee"/>
+
+        <include layout="@layout/search_breadcrumb_view"/>
+    </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/search_main.xml b/res/layout/search_main.xml
new file mode 100644
index 0000000..6de344d
--- /dev/null
+++ b/res/layout/search_main.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright 2016, 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.
+*/
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/main_content"
+    android:layout_height="match_parent"
+    android:layout_width="match_parent"/>
diff --git a/res/layout/search_panel.xml b/res/layout/search_panel.xml
new file mode 100644
index 0000000..b5933cc
--- /dev/null
+++ b/res/layout/search_panel.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/search_panel"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <FrameLayout
+        android:id="@+id/search_bar_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="@color/search_panel_background"
+        android:elevation="4dp">
+        <android.support.v7.widget.CardView
+            android:id="@+id/search_bar"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/search_bar_margin"
+            app:cardCornerRadius="2dp"
+            app:cardBackgroundColor="?android:attr/colorBackground"
+            app:cardElevation="2dp">
+            <Toolbar
+                android:id="@+id/search_toolbar"
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/search_bar_height"
+                android:background="?android:attr/selectableItemBackground"
+                android:contentInsetStart="0dp"
+                android:contentInsetStartWithNavigation="0dp"
+                android:theme="?android:attr/actionBarTheme">
+                <SearchView
+                    android:id="@+id/search_view"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:iconifiedByDefault="false"
+                    android:imeOptions="actionSearch|flagNoExtractUi"
+                    android:searchIcon="@null"/>
+            </Toolbar>
+        </android.support.v7.widget.CardView>
+    </FrameLayout>
+
+    <FrameLayout
+        android:id="@+id/layout_results"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:orientation="vertical">
+
+        <!-- Padding is included in the background -->
+        <android.support.v7.widget.RecyclerView
+            android:id="@+id/list_results"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingStart="@dimen/dashboard_padding_start"
+            android:paddingEnd="@dimen/dashboard_padding_end"
+            android:paddingTop="@dimen/dashboard_padding_top"
+            android:paddingBottom="@dimen/dashboard_padding_bottom"
+            android:scrollbarStyle="outsideOverlay"
+            android:scrollbars="vertical"/>
+
+        <LinearLayout
+            android:id="@+id/no_results_layout"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:paddingTop="35dp"
+            android:orientation="vertical"
+            android:visibility="gone">
+
+            <Space
+                android:layout_width="match_parent"
+                android:layout_height="?android:attr/actionBarSize"/>
+
+            <ImageView
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                android:layout_gravity="center_horizontal"
+                android:src="@drawable/empty_search_results"/>
+
+            <TextView
+                android:layout_height="wrap_content"
+                android:layout_width="match_parent"
+                android:paddingTop="24dp"
+                android:textSize="18sp"
+                android:text="@string/search_no_results"
+                android:gravity="center"/>
+
+        </LinearLayout>
+
+    </FrameLayout>
+
+    <include layout="@layout/search_feedback"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/search_saved_query_item.xml b/res/layout/search_saved_query_item.xml
new file mode 100644
index 0000000..f8c239a
--- /dev/null
+++ b/res/layout/search_saved_query_item.xml
@@ -0,0 +1,44 @@
+<?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:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:background="?android:attr/selectableItemBackground"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:gravity="center_vertical">
+
+    <ImageView
+        android:id="@android:id/icon"
+        android:layout_width="@dimen/dashboard_tile_image_size"
+        android:layout_height="@dimen/dashboard_tile_image_size"
+        android:scaleType="centerInside"
+        android:layout_marginStart="@dimen/dashboard_tile_image_margin"
+        android:layout_marginEnd="@dimen/dashboard_tile_image_margin"
+        android:src="@drawable/ic_restore" />
+
+    <TextView
+        android:id="@android:id/title"
+        android:layout_height="match_parent"
+        android:layout_width="match_parent"
+        android:gravity="center_vertical"
+        android:textAppearance="?android:attr/textAppearanceListItem" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
new file mode 100644
index 0000000..947fadf
--- /dev/null
+++ b/res/values/attrs.xml
@@ -0,0 +1,23 @@
+<?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.
+-->
+
+<resources>
+<!-- For Search -->
+    <declare-styleable name="Preference">
+        <attr name="keywords" format="string" />
+    </declare-styleable>
+</resources>
\ No newline at end of file
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 7013b15..e090c23 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -17,4 +17,6 @@
 
 <resources>
     <color name="launcher_background_color">#ff009688</color>
+
+    <color name="search_panel_background">#f2f2f2</color>
 </resources>
\ No newline at end of file
diff --git a/res/values/configs.xml b/res/values/configs.xml
new file mode 100644
index 0000000..eaafe2f
--- /dev/null
+++ b/res/values/configs.xml
@@ -0,0 +1,21 @@
+<?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.
+  -->
+
+<resources>
+    <!-- Fully-qualified class name for the implementation of the FeatureFactory to be instantiated. -->
+    <string name="config_featureFactory" translatable="false">com.android.settings.intelligence.overlay.FeatureFactoryImpl</string>
+</resources>
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
new file mode 100644
index 0000000..8a40eb6
--- /dev/null
+++ b/res/values/dimens.xml
@@ -0,0 +1,40 @@
+<?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.
+  -->
+
+<resources>
+
+    <!-- Dashboard padding in its container -->
+    <dimen name="dashboard_padding_start">0dp</dimen>
+    <dimen name="dashboard_padding_end">0dp</dimen>
+    <dimen name="dashboard_padding_top">0dp</dimen>
+    <dimen name="dashboard_padding_bottom">0dp</dimen>
+
+    <!-- The following two margins need to match, with the caveat that
+     the second should be negative. The second one ensures that the icons and text
+     align despite the additional padding caused by the search bar's card background. -->
+    <dimen name="search_bar_margin">8dp</dimen>
+    <dimen name="search_bar_negative_margin">-8dp</dimen>
+
+    <dimen name="search_bar_height">48dp</dimen>
+    <dimen name="search_bar_text_size">16dp</dimen>
+
+    <!-- Dashboard image tile size -->
+    <dimen name="dashboard_tile_image_size">24dp</dimen>
+
+    <!-- Dashboard tile image margin start / end -->
+    <dimen name="dashboard_tile_image_margin">24dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 7fa89ab..3c05a31 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -15,7 +15,34 @@
   limitations under the License.
   -->
 
-<resources>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Applications settings title. If clicked, the user is taken to a screen full of application settings [CHAR_LIMIT=NONE BACKUP_MESSAGE_ID=5281808652705396152] -->
+    <string name="applications_settings">App info</string>
+
+    <!-- Settings title in main settings screen for accessibility settings [CHAR_LIMIT=NONE BACKUP_MESSAGE_ID=3975902491934816215] -->
+    <string name="accessibility_settings">Accessibility</string>
+
+    <!-- Title for the 'physical keyboard' settings screen. [CHAR LIMIT=35 BACKUP_MESSAGE_ID=8285149877925752042] -->
+    <string name="physical_keyboard_title">Physical keyboard</string>
+
+    <!-- Title for the button to trigger the 'Manage keyboards' preference sub-screen, where the user can turn on/off installed virtual keyboards.[CHAR LIMIT=35 BACKUP_MESSAGE_ID=3302152381456516928] -->
+    <string name="add_virtual_keyboard">Manage keyboards</string>
+
     <!-- App name of SettingsIntelligence [CHAR_LIMIT=30] -->
     <string name="app_name_settings_intelligence">Settings Intelligence</string>
+
+    <!-- SEARCH STRINGS -->
+
+    <!-- Button to clear all search history in Settings [CHAR LIMIT=40]-->
+    <string name="search_clear_history">Clear history</string>
+
+    <!-- DO NOT TRANSLATE Summary placeholder -->
+    <string name="summary_placeholder" translatable="false">&#160;</string>
+
+    <!-- Search breadcrumb connector symbol -->
+    <string name="search_breadcrumb_connector" translatable="false">
+        <xliff:g name="first_item">%1$s</xliff:g> > <xliff:g name="second_item">%2$s</xliff:g>
+    </string>
+
+    <string name="search_no_results">No results</string>
 </resources>
\ No newline at end of file
diff --git a/res/values/themes.xml b/res/values/themes.xml
new file mode 100644
index 0000000..4e8ae4e
--- /dev/null
+++ b/res/values/themes.xml
@@ -0,0 +1,25 @@
+<?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.
+  -->
+
+<resources>
+    <style name="Theme.Settings" parent="@android:style/Theme.DeviceDefault.Settings"/>
+
+    <style name="Theme.Settings.NoActionBar">
+        <item name="android:windowActionBar">false</item>
+        <item name="android:windowNoTitle">true</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/SettingsIntelligenceDumpService.java b/src/com/android/settings/intelligence/SettingsIntelligenceDumpService.java
new file mode 100644
index 0000000..3ece045
--- /dev/null
+++ b/src/com/android/settings/intelligence/SettingsIntelligenceDumpService.java
@@ -0,0 +1,86 @@
+/*
+ * 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;
+
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.IBinder;
+import android.support.annotation.VisibleForTesting;
+
+import com.android.settings.intelligence.suggestions.model.SuggestionCategory;
+import com.android.settings.intelligence.suggestions.model.SuggestionCategoryRegistry;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * A service that reacts to adb shell dumpsys.
+ * <p/>
+ * adb shell am startservice com.android.settings.intelligence/\
+ *      .SettingsIntelligenceDumpService
+ * adb shell dumpsys activity service com.android.settings.intelligence/\
+ *      .SettingsIntelligenceDumpService
+ */
+public class SettingsIntelligenceDumpService extends Service {
+
+    private static final String KEY_SUGGESTION_CATEGORY = "suggestion_category: ";
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        final StringBuilder dump = new StringBuilder();
+        dump.append(getString(R.string.app_name_settings_intelligence))
+                .append('\n')
+                .append(dumpSuggestions());
+        writer.println(dump.toString());
+    }
+
+    @VisibleForTesting
+    String dumpSuggestions() {
+        final StringBuilder dump = new StringBuilder();
+        final PackageManager pm = getPackageManager();
+        dump.append(" suggestion dump\n");
+        for (SuggestionCategory category : SuggestionCategoryRegistry.CATEGORIES) {
+            dump.append(KEY_SUGGESTION_CATEGORY)
+                    .append(category.getCategory())
+                    .append('\n');
+
+            final Intent probe = new Intent(Intent.ACTION_MAIN);
+            probe.addCategory(category.getCategory());
+            final List<ResolveInfo> results = pm
+                    .queryIntentActivities(probe, PackageManager.GET_META_DATA);
+            if (results == null || results.isEmpty()) {
+                continue;
+            }
+            for (ResolveInfo info : results) {
+                dump.append("\t\t")
+                        .append(info.activityInfo.packageName)
+                        .append('/')
+                        .append(info.activityInfo.name)
+                        .append('\n');
+            }
+        }
+        return dump.toString();
+    }
+}
diff --git a/tests/robotests/src/android/service/settings/suggestions/SuggestionService.java b/src/com/android/settings/intelligence/experiment/ExperimentFeatureProvider.java
similarity index 61%
rename from tests/robotests/src/android/service/settings/suggestions/SuggestionService.java
rename to src/com/android/settings/intelligence/experiment/ExperimentFeatureProvider.java
index e74f1fa..f3b9958 100644
--- a/tests/robotests/src/android/service/settings/suggestions/SuggestionService.java
+++ b/src/com/android/settings/intelligence/experiment/ExperimentFeatureProvider.java
@@ -14,18 +14,18 @@
  * limitations under the License.
  */
 
-package android.service.settings.suggestions;
+package com.android.settings.intelligence.experiment;
 
-import android.app.Service;
+import android.content.Context;
 
-import java.util.List;
+public class ExperimentFeatureProvider {
 
-/**
- * Dupe to android.service.settings.suggestions.SuggestionService to get around robolectric problem.
- */
-public abstract class SuggestionService extends Service {
+    public long getMaxSuggestionDisplayCount(Context context) {
+        return 3;
+    }
 
-    public abstract List<Suggestion> onGetSuggestions();
+    public boolean isPredictiveRingerEnabled(Context context) { return false; }
 
-    public abstract void onSuggestionDismissed(Suggestion suggestion);
+    public boolean isPredictiveBluetoothDrivingEnabled(Context context) { return false; }
+
 }
diff --git a/tests/robotests/src/com/android/settings/intelligence/TestConfig.java b/src/com/android/settings/intelligence/instrumentation/EventLogger.java
similarity index 70%
copy from tests/robotests/src/com/android/settings/intelligence/TestConfig.java
copy to src/com/android/settings/intelligence/instrumentation/EventLogger.java
index 44d238b..3dadee1 100644
--- a/tests/robotests/src/com/android/settings/intelligence/TestConfig.java
+++ b/src/com/android/settings/intelligence/instrumentation/EventLogger.java
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.settings.intelligence;
+package com.android.settings.intelligence.instrumentation;
 
-/**
- * Constants for Robolectric config.
- */
-public class TestConfig {
-    public static final int SDK_VERSION = 26;
-    public static final String MANIFEST_PATH = "--default";
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto
+        .SettingsIntelligenceEvent;
+
+public interface EventLogger {
+
+    void log(SettingsIntelligenceEvent event);
 }
diff --git a/src/com/android/settings/intelligence/instrumentation/LocalEventLogger.java b/src/com/android/settings/intelligence/instrumentation/LocalEventLogger.java
new file mode 100644
index 0000000..22e9eb7
--- /dev/null
+++ b/src/com/android/settings/intelligence/instrumentation/LocalEventLogger.java
@@ -0,0 +1,31 @@
+/*
+ * 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.instrumentation;
+
+import android.util.Log;
+
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto;
+
+public class LocalEventLogger implements EventLogger {
+
+    private static final String TAG = "SettingsIntLogLocal";
+
+    @Override
+    public void log(SettingsIntelligenceLogProto.SettingsIntelligenceEvent event) {
+        Log.i(TAG, event.toString());
+    }
+}
diff --git a/src/com/android/settings/intelligence/instrumentation/MetricsFeatureProvider.java b/src/com/android/settings/intelligence/instrumentation/MetricsFeatureProvider.java
new file mode 100644
index 0000000..6231620
--- /dev/null
+++ b/src/com/android/settings/intelligence/instrumentation/MetricsFeatureProvider.java
@@ -0,0 +1,98 @@
+/*
+ * 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.instrumentation;
+
+import android.content.Context;
+import android.text.TextUtils;
+
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto
+        .SettingsIntelligenceEvent;
+import com.android.settings.intelligence.search.SearchResult;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MetricsFeatureProvider {
+
+    protected Context mContext;
+    protected List<EventLogger> mLoggers;
+
+    public MetricsFeatureProvider(Context context) {
+        mContext = context;
+        mLoggers = new ArrayList<>();
+        mLoggers.add(new LocalEventLogger());
+    }
+
+    public void logGetSuggestion(List<String> ids, long latency) {
+        final SettingsIntelligenceEvent event = new SettingsIntelligenceEvent();
+        event.eventType = SettingsIntelligenceEvent.GET_SUGGESTION;
+        event.latencyMillis = latency;
+        if (ids != null) {
+            event.suggestionIds = ids.toArray(new String[0]);
+        }
+        logEvent(event);
+    }
+
+    public void logDismissSuggestion(String id, long latency) {
+        final SettingsIntelligenceEvent event = new SettingsIntelligenceEvent();
+        event.eventType = SettingsIntelligenceEvent.DISMISS_SUGGESTION;
+        event.latencyMillis = latency;
+        if (!TextUtils.isEmpty(id)) {
+            event.suggestionIds = new String[]{id};
+        }
+        logEvent(event);
+    }
+
+    public void logLaunchSuggestion(String id, long latency) {
+        final SettingsIntelligenceEvent event = new SettingsIntelligenceEvent();
+        event.eventType = SettingsIntelligenceEvent.LAUNCH_SUGGESTION;
+        event.latencyMillis = latency;
+        if (!TextUtils.isEmpty(id)) {
+            event.suggestionIds = new String[]{id};
+        }
+        logEvent(event);
+    }
+
+    public void logSearchResultClick(SearchResult result, String query, int type, int count,
+            int rank) {
+        final SettingsIntelligenceEvent event = new SettingsIntelligenceEvent();
+        event.eventType = type;
+        event.searchResultMetadata = new SettingsIntelligenceEvent.SearchResultMetadata();
+        event.searchResultMetadata.resultCount = count;
+        event.searchResultMetadata.searchResultRank = rank;
+        event.searchResultMetadata.searchResultKey = result.dataKey != null ? result.dataKey : "";
+        event.searchResultMetadata.searchQueryLength = query != null ? query.length() : 0;
+        logEvent(event);
+    }
+
+    public void logEvent(int eventType) {
+        logEvent(eventType, 0 /* latency */);
+    }
+
+    public void logEvent(int eventType, long latency) {
+        final SettingsIntelligenceEvent event = new SettingsIntelligenceEvent();
+        event.eventType = eventType;
+        event.latencyMillis = latency;
+        logEvent(event);
+    }
+
+    private void logEvent(SettingsIntelligenceEvent event) {
+        for (EventLogger logger : mLoggers) {
+            logger.log(event);
+        }
+    }
+}
diff --git a/src/com/android/settings/intelligence/overlay/FeatureFactory.java b/src/com/android/settings/intelligence/overlay/FeatureFactory.java
new file mode 100644
index 0000000..e144f39
--- /dev/null
+++ b/src/com/android/settings/intelligence/overlay/FeatureFactory.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.intelligence.overlay;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.experiment.ExperimentFeatureProvider;
+import com.android.settings.intelligence.instrumentation.MetricsFeatureProvider;
+import com.android.settings.intelligence.search.SearchFeatureProvider;
+import com.android.settings.intelligence.suggestions.SuggestionFeatureProvider;
+
+public abstract class FeatureFactory {
+
+    private static final String LOG_TAG = "FeatureFactory";
+    private static final boolean DEBUG = false;
+
+    protected static FeatureFactory sFactory;
+
+    /**
+     * Returns a factory for creating feature controllers. Creates the factory if it does not
+     * already exist. Uses the value of {@link R.string#config_featureFactory} to instantiate
+     * a factory implementation.
+     */
+    public static FeatureFactory get(Context context) {
+        if (sFactory != null) {
+            return sFactory;
+        }
+
+        if (DEBUG) {
+            Log.d(LOG_TAG, "getFactory");
+        }
+        final String clsName = context.getString(R.string.config_featureFactory);
+        if (TextUtils.isEmpty(clsName)) {
+            throw new UnsupportedOperationException("No feature factory configured");
+        }
+        try {
+            sFactory = (FeatureFactory) context.getClassLoader().loadClass(clsName).newInstance();
+        } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
+            throw new FactoryNotFoundException(e);
+        }
+
+        if (DEBUG) {
+            Log.d(LOG_TAG, "started " + sFactory.getClass().getSimpleName());
+        }
+        return sFactory;
+    }
+
+    public abstract MetricsFeatureProvider metricsFeatureProvider(Context context);
+
+    public abstract SuggestionFeatureProvider suggestionFeatureProvider();
+
+    public abstract ExperimentFeatureProvider experimentFeatureProvider();
+
+    public abstract SearchFeatureProvider searchFeatureProvider();
+
+    public static final class FactoryNotFoundException extends RuntimeException {
+        public FactoryNotFoundException(Throwable throwable) {
+            super("Unable to create factory. Did you misconfigure Proguard?", throwable);
+        }
+    }
+
+}
diff --git a/src/com/android/settings/intelligence/overlay/FeatureFactoryImpl.java b/src/com/android/settings/intelligence/overlay/FeatureFactoryImpl.java
new file mode 100644
index 0000000..99798e7
--- /dev/null
+++ b/src/com/android/settings/intelligence/overlay/FeatureFactoryImpl.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.intelligence.overlay;
+
+import android.content.Context;
+import android.support.annotation.Keep;
+
+import com.android.settings.intelligence.experiment.ExperimentFeatureProvider;
+import com.android.settings.intelligence.instrumentation.MetricsFeatureProvider;
+import com.android.settings.intelligence.search.SearchFeatureProvider;
+import com.android.settings.intelligence.search.SearchFeatureProviderImpl;
+import com.android.settings.intelligence.suggestions.SuggestionFeatureProvider;
+
+@Keep
+public class FeatureFactoryImpl extends FeatureFactory {
+
+    protected MetricsFeatureProvider mMetricsFeatureProvider;
+    protected SuggestionFeatureProvider mSuggestionFeatureProvider;
+    protected ExperimentFeatureProvider mExperimentFeatureProvider;
+    protected SearchFeatureProvider mSearchFeatureProvider;
+
+    @Override
+    public MetricsFeatureProvider metricsFeatureProvider(Context context) {
+        if (mMetricsFeatureProvider == null) {
+            mMetricsFeatureProvider = new MetricsFeatureProvider(context.getApplicationContext());
+        }
+        return mMetricsFeatureProvider;
+    }
+
+    @Override
+    public SuggestionFeatureProvider suggestionFeatureProvider() {
+        if (mSuggestionFeatureProvider == null) {
+            mSuggestionFeatureProvider = new SuggestionFeatureProvider();
+        }
+        return mSuggestionFeatureProvider;
+    }
+
+    @Override
+    public ExperimentFeatureProvider experimentFeatureProvider() {
+        if (mExperimentFeatureProvider == null) {
+            mExperimentFeatureProvider = new ExperimentFeatureProvider();
+        }
+        return mExperimentFeatureProvider;
+    }
+
+    @Override
+    public SearchFeatureProvider searchFeatureProvider() {
+        if (mSearchFeatureProvider == null) {
+            mSearchFeatureProvider = new SearchFeatureProviderImpl();
+        }
+        return mSearchFeatureProvider;
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/AppSearchResult.java b/src/com/android/settings/intelligence/search/AppSearchResult.java
new file mode 100644
index 0000000..567fd70
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/AppSearchResult.java
@@ -0,0 +1,45 @@
+/*
+ * 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 android.content.pm.ApplicationInfo;
+
+public class AppSearchResult extends SearchResult {
+    /**
+     * Installed app's ApplicationInfo for delayed loading of icons
+     */
+    public final ApplicationInfo info;
+
+    public AppSearchResult(Builder builder) {
+        super(builder);
+        info = builder.mInfo;
+    }
+
+    public static class Builder extends SearchResult.Builder {
+        protected ApplicationInfo mInfo;
+
+        public SearchResult.Builder setAppInfo(ApplicationInfo info) {
+            mInfo = info;
+            return this;
+        }
+
+        public AppSearchResult build() {
+            return new AppSearchResult(this);
+        }
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/IntentSearchViewHolder.java b/src/com/android/settings/intelligence/search/IntentSearchViewHolder.java
new file mode 100644
index 0000000..ec02164
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/IntentSearchViewHolder.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.intelligence.search;
+
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+import android.view.View;
+
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto;
+
+import java.util.List;
+
+/**
+ * ViewHolder for intent based search results.
+ * The DatabaseResultTask is the primary use case for this ViewHolder.
+ */
+public class IntentSearchViewHolder extends SearchViewHolder {
+
+    private static final String TAG = "IntentSearchViewHolder";
+    @VisibleForTesting
+    static final int REQUEST_CODE_NO_OP = 0;
+
+    public IntentSearchViewHolder(View view) {
+        super(view);
+    }
+
+    @Override
+    public int getClickActionMetricName() {
+        return SettingsIntelligenceLogProto.SettingsIntelligenceEvent.CLICK_SEARCH_RESULT;
+    }
+
+    @Override
+    public void onBind(final SearchFragment fragment, final SearchResult result) {
+        super.onBind(fragment, result);
+        final SearchViewHolder viewHolder = this;
+
+        // TODO (b/64935342) add dynamic api
+        itemView.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                fragment.onSearchResultClicked(viewHolder, result);
+                final Intent intent = result.payload.getIntent();
+                // Use app user id to support work profile use case.
+                if (result instanceof AppSearchResult) {
+                    AppSearchResult appResult = (AppSearchResult) result;
+                    fragment.getActivity().startActivity(intent);
+                } else {
+                    final PackageManager pm = fragment.getActivity().getPackageManager();
+                    final List<ResolveInfo> info = pm.queryIntentActivities(intent, 0 /* flags */);
+                    if (info != null && !info.isEmpty()) {
+                        fragment.startActivityForResult(intent, REQUEST_CODE_NO_OP);
+                    } else {
+                        Log.e(TAG, "Cannot launch search result, title: "
+                                + result.title + ", " + intent);
+                    }
+                }
+            }
+        });
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/ResultPayload.java b/src/com/android/settings/intelligence/search/ResultPayload.java
new file mode 100644
index 0000000..da5ff60
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/ResultPayload.java
@@ -0,0 +1,165 @@
+/*
+ * 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 android.support.annotation.IntDef;
+import android.content.Intent;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A interface for search results types. Examples include Inline results, third party apps
+ * or any future possibilities.
+ */
+public class ResultPayload implements Parcelable {
+    protected final Intent mIntent;
+
+    @IntDef({PayloadType.INTENT, PayloadType.INLINE_SLIDER, PayloadType.INLINE_SWITCH,
+            PayloadType.INLINE_LIST, PayloadType.SAVED_QUERY})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PayloadType {
+        /**
+         * Resulting page will be started using an mIntent
+         */
+        int INTENT = 0;
+
+        /**
+         * Result is a inline widget, using a slider widget as UI.
+         */
+        int INLINE_SLIDER = 1;
+
+        /**
+         * Result is a inline widget, using a toggle widget as UI.
+         */
+        int INLINE_SWITCH = 2;
+
+        /**
+         * Result is an inline list-select, with an undefined UI.
+         */
+        int INLINE_LIST = 3;
+
+        /**
+         * Result is a recently saved query.
+         */
+        int SAVED_QUERY = 4;
+    }
+
+    /**
+     * Enumerates the possible values for the Availability of a setting.
+     */
+    @IntDef({Availability.AVAILABLE,
+            Availability.DISABLED_DEPENDENT_SETTING,
+            Availability.DISABLED_DEPENDENT_APP,
+            Availability.DISABLED_UNSUPPORTED,
+            Availability.RESOURCE_CONTENTION,
+            Availability.INTENT_ONLY,
+            Availability.DISABLED_FOR_USER,})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Availability {
+        /**
+         * The setting is available.
+         */
+        int AVAILABLE = 0;
+
+        /**
+         * The setting has a dependency in settings app which is currently disabled, blocking
+         * access.
+         */
+        int DISABLED_DEPENDENT_SETTING = 1;
+
+        /**
+         * The setting is not supported by the device.
+         */
+        int DISABLED_UNSUPPORTED = 2;
+
+        /**
+         * The setting you are trying to change is being used by another application and cannot
+         * be changed until it is released by said application.
+         */
+        int RESOURCE_CONTENTION = 3;
+
+        /**
+         * The setting is disabled because corresponding app is disabled.
+         */
+        int DISABLED_DEPENDENT_APP = 4;
+
+        /**
+         * This setting is supported on the device but cannot be changed inline.
+         */
+        int INTENT_ONLY = 5;
+
+        /**
+         * The setting cannot be changed by the current user.
+         * ex: MobileNetworkTakeMeThereSetting should not be available to a secondary user.
+         */
+        int DISABLED_FOR_USER = 6;
+    }
+
+    @IntDef({SettingsSource.UNKNOWN, SettingsSource.SYSTEM, SettingsSource.SECURE,
+            SettingsSource.GLOBAL})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SettingsSource {
+        int UNKNOWN = 0;
+        int SYSTEM = 1;
+        int SECURE = 2;
+        int GLOBAL = 3;
+    }
+
+
+    private ResultPayload(Parcel in) {
+        mIntent = in.readParcelable(ResultPayload.class.getClassLoader());
+    }
+
+    public ResultPayload(Intent intent) {
+        mIntent = intent;
+    }
+
+    @ResultPayload.PayloadType
+    public int getType() {
+        return PayloadType.INTENT;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(mIntent, flags);
+    }
+
+    public static final Creator<ResultPayload> CREATOR = new Creator<ResultPayload>() {
+        @Override
+        public ResultPayload createFromParcel(Parcel in) {
+            return new ResultPayload(in);
+        }
+
+        @Override
+        public ResultPayload[] newArray(int size) {
+            return new ResultPayload[size];
+        }
+    };
+
+    public Intent getIntent() {
+        return mIntent;
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/ResultPayloadUtils.java b/src/com/android/settings/intelligence/search/ResultPayloadUtils.java
new file mode 100644
index 0000000..096edd5
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/ResultPayloadUtils.java
@@ -0,0 +1,52 @@
+/*
+ * 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 android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Utility class to Marshall and Unmarshall the payloads stored in the SQLite Database
+ */
+public class ResultPayloadUtils {
+
+    private static final String TAG = "PayloadUtil";
+
+    public static byte[] marshall(ResultPayload payload) {
+        Parcel parcel = Parcel.obtain();
+        payload.writeToParcel(parcel, 0);
+        byte[] bytes = parcel.marshall();
+        parcel.recycle();
+        return bytes;
+    }
+
+    public static <T> T unmarshall(byte[] bytes, Parcelable.Creator<T> creator) {
+        T result;
+        Parcel parcel = unmarshall(bytes);
+        result = creator.createFromParcel(parcel);
+        parcel.recycle();
+        return result;
+    }
+
+    private static Parcel unmarshall(byte[] bytes) {
+        Parcel parcel = Parcel.obtain();
+        parcel.unmarshall(bytes, 0, bytes.length);
+        parcel.setDataPosition(0);
+        return parcel;
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/SearchActivity.java b/src/com/android/settings/intelligence/search/SearchActivity.java
new file mode 100644
index 0000000..3521ef3
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchActivity.java
@@ -0,0 +1,51 @@
+/*
+ * 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 android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.os.Bundle;
+import android.view.WindowManager;
+
+import com.android.settings.intelligence.R;
+
+public class SearchActivity extends Activity {
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.search_main);
+        // Keeps layouts in-place when keyboard opens.
+        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
+
+        FragmentManager fragmentManager = getFragmentManager();
+        Fragment fragment = fragmentManager.findFragmentById(R.id.main_content);
+        if (fragment == null) {
+            fragmentManager.beginTransaction()
+                    .add(R.id.main_content, new SearchFragment())
+                    .commit();
+        }
+    }
+
+    @Override
+    public boolean onNavigateUp() {
+        finish();
+        return true;
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/SearchFeatureProvider.java b/src/com/android/settings/intelligence/search/SearchFeatureProvider.java
new file mode 100644
index 0000000..2ccac53
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchFeatureProvider.java
@@ -0,0 +1,126 @@
+/*
+ * 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 android.content.Context;
+import android.util.Pair;
+import android.view.View;
+
+import com.android.settings.intelligence.search.indexing.DatabaseIndexingManager;
+import com.android.settings.intelligence.search.indexing.IndexingCallback;
+import com.android.settings.intelligence.search.query.SearchQueryTask;
+import com.android.settings.intelligence.search.savedqueries.SavedQueryLoader;
+import com.android.settings.intelligence.search.sitemap.SiteMapManager;
+
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.FutureTask;
+
+/**
+ * FeatureProvider for Settings Search
+ */
+public interface SearchFeatureProvider {
+
+    boolean DEBUG = false;
+
+    /**
+     * Returns a new loader to get settings search results.
+     */
+    SearchResultLoader getSearchResultLoader(Context context, String query);
+
+    /**
+     * Returns a list of {@link SearchQueryTask}, each responsible for searching a subsystem for
+     * user query.
+     */
+    List<SearchQueryTask> getSearchQueryTasks(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);
+
+    /**
+     * Returns the manager for looking up breadcrumbs.
+     */
+    SiteMapManager getSiteMapManager();
+
+    /**
+     * Updates the Settings indexes and calls {@link IndexingCallback#onIndexingFinished()} on
+     * {@param callback} when indexing is complete.
+     */
+    void updateIndexAsync(Context context, IndexingCallback callback);
+
+    /**
+     * @returns true when indexing is complete.
+     */
+    boolean isIndexingComplete(Context context);
+
+    /**
+     * @return a {@link ExecutorService} to be shared between search tasks.
+     */
+    ExecutorService getExecutorService();
+
+    /**
+     * Initializes the feedback button in case it was dismissed.
+     */
+    void initFeedbackButton();
+
+    /**
+     * Show a button users can click to submit feedback on the quality of the search results.
+     */
+    void showFeedbackButton(SearchFragment fragment, View root);
+
+    /**
+     * Hide the feedback button shown by
+     * {@link #showFeedbackButton(SearchFragment fragment, View view) showFeedbackButton}
+     */
+    void hideFeedbackButton(View root);
+
+    /**
+     * Notify that a search result is clicked.
+     *
+     * @param context      application context
+     * @param query        input user query
+     * @param searchResult clicked result
+     */
+    void searchResultClicked(Context context, String query, SearchResult searchResult);
+
+    /**
+     * @return true to enable search ranking.
+     */
+    boolean isSmartSearchRankingEnabled(Context context);
+
+    /**
+     * @return smart ranking timeout in milliseconds.
+     */
+    long smartSearchRankingTimeoutMs(Context context);
+
+    /**
+     * Prepare for search ranking predictions to avoid latency on the first prediction call.
+     */
+    void searchRankingWarmup(Context context);
+
+    /**
+     * Return a FutureTask to get a list of scores for search results.
+     */
+    FutureTask<List<Pair<String, Float>>> getRankerTask(Context context, String query);
+}
diff --git a/src/com/android/settings/intelligence/search/SearchFeatureProviderImpl.java b/src/com/android/settings/intelligence/search/SearchFeatureProviderImpl.java
new file mode 100644
index 0000000..0489117
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchFeatureProviderImpl.java
@@ -0,0 +1,164 @@
+/*
+ * 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 android.content.Context;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import android.view.View;
+
+import com.android.settings.intelligence.search.indexing.DatabaseIndexingManager;
+import com.android.settings.intelligence.search.indexing.IndexData;
+import com.android.settings.intelligence.search.indexing.IndexingCallback;
+import com.android.settings.intelligence.search.query.AccessibilityServiceResultTask;
+import com.android.settings.intelligence.search.query.DatabaseResultTask;
+import com.android.settings.intelligence.search.query.InputDeviceResultTask;
+import com.android.settings.intelligence.search.query.InstalledAppResultTask;
+import com.android.settings.intelligence.search.query.SearchQueryTask;
+import com.android.settings.intelligence.search.savedqueries.SavedQueryLoader;
+import com.android.settings.intelligence.search.sitemap.SiteMapManager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+
+/**
+ * FeatureProvider for the refactored search code.
+ */
+public class SearchFeatureProviderImpl implements SearchFeatureProvider {
+
+    private static final String TAG = "SearchFeatureProvider";
+
+    private DatabaseIndexingManager mDatabaseIndexingManager;
+    private ExecutorService mExecutorService;
+    private SiteMapManager mSiteMapManager;
+
+    @Override
+    public SearchResultLoader getSearchResultLoader(Context context, String query) {
+        return new SearchResultLoader(context, cleanQuery(query));
+    }
+
+    @Override
+    public List<SearchQueryTask> getSearchQueryTasks(Context context, String query) {
+        final List<SearchQueryTask> tasks = new ArrayList<>();
+        final String cleanQuery = cleanQuery(query);
+        tasks.add(DatabaseResultTask.newTask(context, getSiteMapManager(), cleanQuery));
+        tasks.add(InstalledAppResultTask.newTask(context, getSiteMapManager(), cleanQuery));
+        tasks.add(AccessibilityServiceResultTask.newTask(context, getSiteMapManager(), cleanQuery));
+        tasks.add(InputDeviceResultTask.newTask(context, getSiteMapManager(), cleanQuery));
+        return tasks;
+    }
+
+    @Override
+    public SavedQueryLoader getSavedQueryLoader(Context context) {
+        return new SavedQueryLoader(context);
+    }
+
+    @Override
+    public DatabaseIndexingManager getIndexingManager(Context context) {
+        if (mDatabaseIndexingManager == null) {
+            mDatabaseIndexingManager = new DatabaseIndexingManager(context.getApplicationContext());
+        }
+        return mDatabaseIndexingManager;
+    }
+
+    @Override
+    public SiteMapManager getSiteMapManager() {
+        if (mSiteMapManager == null) {
+            mSiteMapManager = new SiteMapManager();
+        }
+        return mSiteMapManager;
+    }
+
+    @Override
+    public boolean isIndexingComplete(Context context) {
+        return getIndexingManager(context).isIndexingComplete();
+    }
+
+    @Override
+    public void initFeedbackButton() {
+    }
+
+    @Override
+    public void showFeedbackButton(SearchFragment fragment, View root) {
+    }
+
+    @Override
+    public void hideFeedbackButton(View root) {
+    }
+
+    @Override
+    public void searchResultClicked(Context context, String query, SearchResult searchResult) {
+    }
+
+    @Override
+    public boolean isSmartSearchRankingEnabled(Context context) {
+        return false;
+    }
+
+    @Override
+    public long smartSearchRankingTimeoutMs(Context context) {
+        return 300L;
+    }
+
+    @Override
+    public void searchRankingWarmup(Context context) {
+    }
+
+    @Override
+    public FutureTask<List<Pair<String, Float>>> getRankerTask(Context context, String query) {
+        return null;
+    }
+
+    @Override
+    public void updateIndexAsync(Context context, IndexingCallback callback) {
+        if (DEBUG) {
+            Log.d(TAG, "updating index async");
+        }
+        getIndexingManager(context).indexDatabase(callback);
+    }
+
+    @Override
+    public ExecutorService getExecutorService() {
+        if (mExecutorService == null) {
+            mExecutorService = Executors.newCachedThreadPool();
+        }
+        return mExecutorService;
+    }
+
+    /**
+     * A generic method to make the query suitable for searching the database.
+     *
+     * @return the cleaned query string
+     */
+    @VisibleForTesting
+    String cleanQuery(String query) {
+        if (TextUtils.isEmpty(query)) {
+            return null;
+        }
+        if (Locale.getDefault().equals(Locale.JAPAN)) {
+            query = IndexData.normalizeJapaneseString(query);
+        }
+        return query.trim();
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/SearchFragment.java b/src/com/android/settings/intelligence/search/SearchFragment.java
new file mode 100644
index 0000000..e92e301
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchFragment.java
@@ -0,0 +1,385 @@
+/*
+ * 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.app.Fragment;
+import android.app.LoaderManager;
+import android.content.Context;
+import android.content.Loader;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.os.Bundle;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.TypedValue;
+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.TextView;
+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";
+
+    // State values
+    private static final String STATE_QUERY = "state_query";
+    private static final String STATE_SHOWING_SAVED_QUERY = "state_showing_saved_query";
+    private static final String STATE_NEVER_ENTERED_QUERY = "state_never_entered_query";
+
+    public static final class SearchLoaderId {
+        // Search Query IDs
+        public static final int SEARCH_RESULT = 1;
+
+        // Saved Query IDs
+        public static final int SAVE_QUERY_TASK = 2;
+        public static final int REMOVE_QUERY_TASK = 3;
+        public static final int SAVED_QUERIES = 4;
+    }
+
+    @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(STATE_QUERY);
+            mNeverEnteredQuery = savedInstanceState.getBoolean(STATE_NEVER_ENTERED_QUERY);
+            mShowingSavedQuery = savedInstanceState.getBoolean(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 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();
+
+        // TODO Refactor
+//        ActionBarShadowController.attachToRecyclerView(
+//                view.findViewById(R.id.search_bar_container), getLifecycle(),
+// mResultsRecyclerView);
+        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(STATE_QUERY, mQuery);
+        outState.putBoolean(STATE_NEVER_ENTERED_QUERY, mNeverEnteredQuery);
+        outState.putBoolean(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(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 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(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);
+        } 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(
+                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();
+            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);
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/SearchIndexableRaw.java b/src/com/android/settings/intelligence/search/SearchIndexableRaw.java
new file mode 100644
index 0000000..46ce0c6
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchIndexableRaw.java
@@ -0,0 +1,64 @@
+/*
+ * 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.intelligence.search;
+
+import android.content.Context;
+import android.provider.SearchIndexableData;
+
+/**
+ * Indexable raw data for Search.
+ *
+ * This is the raw data used by the Indexer and should match its data model.
+ *
+ * See {@link Indexable} and {@link android.provider.SearchIndexableResource}.
+ */
+public class SearchIndexableRaw extends SearchIndexableData {
+
+    /**
+     * Title's raw data.
+     */
+    public String title;
+
+    /**
+     * Summary's raw data when the data is "ON".
+     */
+    public String summaryOn;
+
+    /**
+     * Summary's raw data when the data is "OFF".
+     */
+    public String summaryOff;
+
+    /**
+     * Entries associated with the raw data (when the data can have several values).
+     */
+    public String entries;
+
+    /**
+     * Keywords' raw data.
+     */
+    public String keywords;
+
+    /**
+     * Fragment's or Activity's title associated with the raw data.
+     */
+    public String screenTitle;
+
+    public SearchIndexableRaw(Context context) {
+        super(context);
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/SearchResult.java b/src/com/android/settings/intelligence/search/SearchResult.java
new file mode 100644
index 0000000..14065e8
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchResult.java
@@ -0,0 +1,186 @@
+/*
+ * 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 android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * Data class as an interface for all Search Results.
+ */
+public class SearchResult implements Comparable<SearchResult> {
+
+    private static final String TAG = "SearchResult";
+
+    /**
+     * Defines the lowest rank for a search result to be considered as ranked. Results with ranks
+     * higher than this have no guarantee for sorting order.
+     */
+    public static final int BOTTOM_RANK = 10;
+
+    /**
+     * Defines the highest rank for a search result. Used for special search results only.
+     */
+    public static final int TOP_RANK = 0;
+
+    /**
+     * The title of the result and main text displayed.
+     * Intent Results: Displays as the primary
+     */
+    public final CharSequence title;
+
+    /**
+     * Summary / subtitle text
+     * Intent Results: Displays the text underneath the title
+     */
+    final public CharSequence summary;
+
+    /**
+     * An ordered list of the information hierarchy.
+     * Intent Results: Displayed a hierarchy of selections to reach the setting from the home screen
+     */
+    public final List<String> breadcrumbs;
+
+    /**
+     * A suggestion for the ranking of the result.
+     * Based on Settings Rank:
+     * 1 is a near perfect match
+     * 9 is the weakest match
+     * TODO subject to change
+     */
+    public final int rank;
+
+    /**
+     * Identifier for the recycler view adapter.
+     */
+    @ResultPayload.PayloadType
+    public final int viewType;
+
+    /**
+     * Metadata for the specific result types.
+     */
+    public final ResultPayload payload;
+
+    /**
+     * Result's icon.
+     */
+    public final Drawable icon;
+
+    /**
+     * A unique key for this object.
+     */
+    public final String dataKey;
+
+    protected SearchResult(Builder builder) {
+        dataKey = builder.mDataKey;
+        title = builder.mTitle;
+        summary = builder.mSummary;
+        breadcrumbs = builder.mBreadcrumbs;
+        rank = builder.mRank;
+        icon = builder.mIcon;
+        payload = builder.mResultPayload;
+        viewType = payload.getType();
+    }
+
+    @Override
+    public int compareTo(SearchResult searchResult) {
+        if (searchResult == null) {
+            return -1;
+        }
+        return this.rank - searchResult.rank;
+    }
+
+    @Override
+    public boolean equals(Object that) {
+        if (this == that) {
+            return true;
+        }
+        if (!(that instanceof SearchResult)) {
+            return false;
+        }
+        return TextUtils.equals(dataKey, ((SearchResult) that).dataKey);
+    }
+
+    @Override
+    public int hashCode() {
+        return dataKey.hashCode();
+    }
+
+    public static class Builder {
+        private CharSequence mTitle;
+        private CharSequence mSummary;
+        private List<String> mBreadcrumbs;
+        private int mRank = 42;
+        private ResultPayload mResultPayload;
+        private Drawable mIcon;
+        private String mDataKey;
+
+        public Builder setTitle(CharSequence title) {
+            mTitle = title;
+            return this;
+        }
+
+        public Builder setSummary(CharSequence summary) {
+            mSummary = summary;
+            return this;
+        }
+
+        public Builder addBreadcrumbs(List<String> breadcrumbs) {
+            mBreadcrumbs = breadcrumbs;
+            return this;
+        }
+
+        public Builder setRank(int rank) {
+            if (rank >= 0 && rank <= 9) {
+                mRank = rank;
+            }
+            return this;
+        }
+
+        public Builder setIcon(Drawable icon) {
+            mIcon = icon;
+            return this;
+        }
+
+        public Builder setPayload(ResultPayload payload) {
+            mResultPayload = payload;
+            return this;
+        }
+
+        public Builder setDataKey(String key) {
+            mDataKey = key;
+            return this;
+        }
+
+        public SearchResult build() {
+            // Check that all of the mandatory fields are set.
+            if (TextUtils.isEmpty(mTitle)) {
+                throw new IllegalStateException("SearchResult missing title argument");
+            } else if (TextUtils.isEmpty(mDataKey)) {
+                Log.v(TAG, "No data key on SearchResult with title: " + mTitle);
+                throw new IllegalStateException("SearchResult missing stableId argument");
+            } else if (mResultPayload == null) {
+                throw new IllegalStateException("SearchResult missing Payload argument");
+            }
+            return new SearchResult(this);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/search/SearchResultAggregator.java b/src/com/android/settings/intelligence/search/SearchResultAggregator.java
new file mode 100644
index 0000000..20b156c
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchResultAggregator.java
@@ -0,0 +1,107 @@
+package com.android.settings.intelligence.search;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.settings.intelligence.overlay.FeatureFactory;
+import com.android.settings.intelligence.search.query.DatabaseResultTask;
+import com.android.settings.intelligence.search.query.SearchQueryTask;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Collects the sorted list of all setting search results.
+ */
+public class SearchResultAggregator {
+
+    private static final String TAG = "SearchResultAggregator";
+
+    /**
+     * Timeout for subsequent tasks to allow for fast returning tasks.
+     * TODO(70164062): Tweak the timeout values.
+     */
+    private static final long SHORT_CHECK_TASK_TIMEOUT_MS = 300;
+
+    private static SearchResultAggregator sResultAggregator;
+
+    private SearchResultAggregator() {
+    }
+
+    public static SearchResultAggregator getInstance() {
+        if (sResultAggregator == null) {
+            sResultAggregator = new SearchResultAggregator();
+        }
+
+        return sResultAggregator;
+    }
+
+    @NonNull
+    public synchronized List<? extends SearchResult> fetchResults(Context context, String query) {
+        final SearchFeatureProvider mFeatureProvider = FeatureFactory.get(context)
+                .searchFeatureProvider();
+        final ExecutorService executorService = mFeatureProvider.getExecutorService();
+
+        final List<SearchQueryTask> tasks =
+                mFeatureProvider.getSearchQueryTasks(context, query);
+        // Start tasks
+        for (SearchQueryTask task : tasks) {
+            executorService.execute(task);
+        }
+
+        // Collect results
+        final Map<Integer, List<? extends SearchResult>> taskResults = new ArrayMap<>();
+        final long allTasksStart = System.currentTimeMillis();
+        for (SearchQueryTask task : tasks) {
+            final int taskId = task.getTaskId();
+            try {
+                taskResults.put(taskId,
+                        task.get(SHORT_CHECK_TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            } catch (TimeoutException | InterruptedException | ExecutionException e) {
+                Log.d(TAG, "Could not retrieve result in time: " + taskId, e);
+                taskResults.put(taskId, Collections.EMPTY_LIST);
+            }
+        }
+
+        // Merge results
+        final long mergeStartTime = System.currentTimeMillis();
+        if (SearchFeatureProvider.DEBUG) {
+            Log.d(TAG, "Total result loader time: " + (mergeStartTime - allTasksStart));
+        }
+        final List<? extends SearchResult> mergedResults = mergeSearchResults(taskResults);
+        if (SearchFeatureProvider.DEBUG) {
+            Log.d(TAG, "Total merge time: " + (System.currentTimeMillis() - mergeStartTime));
+            Log.d(TAG, "Total aggregator time: " + (System.currentTimeMillis() - allTasksStart));
+        }
+
+        return mergedResults;
+    }
+
+    // TODO (b/68255021) scale the dynamic search results ranks
+    private List<? extends SearchResult> mergeSearchResults(
+            Map<Integer, List<? extends SearchResult>> taskResults) {
+
+        final List<SearchResult> searchResults = new ArrayList<>();
+        // First add db results as a special case
+        searchResults.addAll(taskResults.remove(DatabaseResultTask.QUERY_WORKER_ID));
+
+        // Merge the rest into result list: add everything to heap then pop them out one by one.
+        final PriorityQueue<SearchResult> heap = new PriorityQueue<>();
+        for (List<? extends SearchResult> taskResult : taskResults.values()) {
+            heap.addAll(taskResult);
+        }
+        while (!heap.isEmpty()) {
+            searchResults.add(heap.poll());
+        }
+        return searchResults;
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/SearchResultDiffCallback.java b/src/com/android/settings/intelligence/search/SearchResultDiffCallback.java
new file mode 100644
index 0000000..1d2b946
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchResultDiffCallback.java
@@ -0,0 +1,57 @@
+/*
+ * 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 android.support.v7.util.DiffUtil;
+
+import java.util.List;
+
+/**
+ * Callback for DiffUtil to elegantly update search data when the query changes.
+ */
+public class SearchResultDiffCallback extends DiffUtil.Callback {
+
+    private List<? extends SearchResult> mOldList;
+    private List<? extends SearchResult> mNewList;
+
+    public SearchResultDiffCallback(List<? extends SearchResult> oldList,
+            List<? extends SearchResult> newList) {
+        mOldList = oldList;
+        mNewList = newList;
+    }
+
+    @Override
+    public int getOldListSize() {
+        return mOldList.size();
+    }
+
+    @Override
+    public int getNewListSize() {
+        return mNewList.size();
+    }
+
+    @Override
+    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
+        return mOldList.get(oldItemPosition).equals(mNewList.get(newItemPosition));
+    }
+
+    @Override
+    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
+        return mOldList.get(oldItemPosition).equals(mNewList.get(newItemPosition));
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/SearchResultLoader.java b/src/com/android/settings/intelligence/search/SearchResultLoader.java
new file mode 100644
index 0000000..92104cd
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchResultLoader.java
@@ -0,0 +1,31 @@
+package com.android.settings.intelligence.search;
+
+
+import android.content.Context;
+
+import com.android.settings.intelligence.utils.AsyncLoader;
+
+import java.util.List;
+
+/**
+ * Loads a sorted list of Search results for a given query.
+ */
+public class SearchResultLoader extends AsyncLoader<List<? extends SearchResult>> {
+
+    private final String mQuery;
+
+    public SearchResultLoader(Context context, String query) {
+        super(context);
+        mQuery = query;
+    }
+
+    @Override
+    public List<? extends SearchResult> loadInBackground() {
+        SearchResultAggregator aggregator = SearchResultAggregator.getInstance();
+        return aggregator.fetchResults(getContext(), mQuery);
+    }
+
+    @Override
+    protected void onDiscardResult(List<? extends SearchResult> result) {
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/SearchResultsAdapter.java b/src/com/android/settings/intelligence/search/SearchResultsAdapter.java
new file mode 100644
index 0000000..decf730
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchResultsAdapter.java
@@ -0,0 +1,117 @@
+/*
+ * 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 android.content.Context;
+import android.support.v7.util.DiffUtil;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.search.savedqueries.SavedQueryViewHolder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SearchResultsAdapter extends RecyclerView.Adapter<SearchViewHolder> {
+
+    private final SearchFragment mFragment;
+    private final List<SearchResult> mSearchResults;
+
+    public SearchResultsAdapter(SearchFragment fragment) {
+        mFragment = fragment;
+        mSearchResults = new ArrayList<>();
+
+        setHasStableIds(true);
+    }
+
+    @Override
+    public SearchViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        final Context context = parent.getContext();
+        final LayoutInflater inflater = LayoutInflater.from(context);
+        final View view;
+        switch (viewType) {
+            case ResultPayload.PayloadType.INTENT:
+                view = inflater.inflate(R.layout.search_intent_item, parent, false);
+                return new IntentSearchViewHolder(view);
+            case ResultPayload.PayloadType.INLINE_SWITCH:
+                // TODO (b/62807132) replace layout InlineSwitchViewHolder and return an
+                // InlineSwitchViewHolder.
+                view = inflater.inflate(R.layout.search_intent_item, parent, false);
+                return new IntentSearchViewHolder(view);
+            case ResultPayload.PayloadType.INLINE_LIST:
+                // TODO (b/62807132) build a inline-list view holder & layout.
+                view = inflater.inflate(R.layout.search_intent_item, parent, false);
+                return new IntentSearchViewHolder(view);
+            case ResultPayload.PayloadType.SAVED_QUERY:
+                view = inflater.inflate(R.layout.search_saved_query_item, parent, false);
+                return new SavedQueryViewHolder(view);
+            default:
+                return null;
+        }
+    }
+
+    @Override
+    public void onBindViewHolder(SearchViewHolder holder, int position) {
+        holder.onBind(mFragment, mSearchResults.get(position));
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return mSearchResults.get(position).hashCode();
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        return mSearchResults.get(position).viewType;
+    }
+
+    @Override
+    public int getItemCount() {
+        return mSearchResults.size();
+    }
+
+    /**
+     * Displays recent searched queries.
+     */
+    public void displaySavedQuery(List<? extends SearchResult> data) {
+        clearResults();
+        mSearchResults.addAll(data);
+        notifyDataSetChanged();
+    }
+
+    public void clearResults() {
+        mSearchResults.clear();
+        notifyDataSetChanged();
+    }
+
+    public void postSearchResults(List<? extends SearchResult> newSearchResults) {
+        final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
+                new SearchResultDiffCallback(mSearchResults, newSearchResults));
+        mSearchResults.clear();
+        mSearchResults.addAll(newSearchResults);
+        diffResult.dispatchUpdatesTo(this);
+        mFragment.onSearchResultsDisplayed(mSearchResults.size());
+    }
+
+    public List<SearchResult> getSearchResults() {
+        return mSearchResults;
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/SearchViewHolder.java b/src/com/android/settings/intelligence/search/SearchViewHolder.java
new file mode 100644
index 0000000..c74391e
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchViewHolder.java
@@ -0,0 +1,102 @@
+/*
+ * 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 android.content.Context;
+import android.content.pm.PackageManager;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.overlay.FeatureFactory;
+
+/**
+ * The ViewHolder for the Search RecyclerView.
+ * There are multiple search result types in the same Recycler view with different UI requirements.
+ * Some examples include Intent results, Inline results, and Help articles.
+ */
+public abstract class SearchViewHolder extends RecyclerView.ViewHolder {
+
+    private final String DYNAMIC_PLACEHOLDER = "%s";
+
+    private final String mPlaceholderSummary;
+
+    public final TextView titleView;
+    public final TextView summaryView;
+    public final TextView breadcrumbView;
+    public final ImageView iconView;
+
+    protected final SearchFeatureProvider mSearchFeatureProvider;
+
+    public SearchViewHolder(View view) {
+        super(view);
+        final FeatureFactory featureFactory = FeatureFactory
+                .get(view.getContext().getApplicationContext());
+        mSearchFeatureProvider = featureFactory.searchFeatureProvider();
+        titleView = view.findViewById(android.R.id.title);
+        summaryView = view.findViewById(android.R.id.summary);
+        iconView = view.findViewById(android.R.id.icon);
+        breadcrumbView = view.findViewById(R.id.breadcrumb);
+
+        mPlaceholderSummary = view.getContext().getString(R.string.summary_placeholder);
+    }
+
+    public abstract int getClickActionMetricName();
+
+    public void onBind(SearchFragment fragment, SearchResult result) {
+        titleView.setText(result.title);
+        // TODO (b/36101902) remove check for DYNAMIC_PLACEHOLDER
+        if (TextUtils.isEmpty(result.summary)
+                || TextUtils.equals(result.summary, mPlaceholderSummary)
+                || TextUtils.equals(result.summary, DYNAMIC_PLACEHOLDER)) {
+            summaryView.setVisibility(View.GONE);
+        } else {
+            summaryView.setText(result.summary);
+            summaryView.setVisibility(View.VISIBLE);
+        }
+
+        if (result instanceof AppSearchResult) {
+            AppSearchResult appResult = (AppSearchResult) result;
+            PackageManager pm = fragment.getActivity().getPackageManager();
+            iconView.setImageDrawable(appResult.info.loadIcon(pm));
+        } else {
+            // Valid even when result.icon is null.
+            iconView.setImageDrawable(result.icon);
+        }
+
+        bindBreadcrumbView(result);
+    }
+
+    private void bindBreadcrumbView(SearchResult result) {
+        if (result.breadcrumbs == null || result.breadcrumbs.isEmpty()) {
+            breadcrumbView.setVisibility(View.GONE);
+            return;
+        }
+        final Context context = breadcrumbView.getContext();
+        String breadcrumb = result.breadcrumbs.get(0);
+        final int count = result.breadcrumbs.size();
+        for (int i = 1; i < count; i++) {
+            breadcrumb = context.getString(R.string.search_breadcrumb_connector,
+                    breadcrumb, result.breadcrumbs.get(i));
+        }
+        breadcrumbView.setText(breadcrumb);
+        breadcrumbView.setVisibility(View.VISIBLE);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/search/indexing/DatabaseIndexingManager.java b/src/com/android/settings/intelligence/search/indexing/DatabaseIndexingManager.java
new file mode 100644
index 0000000..0928611
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/indexing/DatabaseIndexingManager.java
@@ -0,0 +1,348 @@
+/*
+ * 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.indexing;
+
+import static com.android.settings.intelligence.search.query.DatabaseResultTask.SELECT_COLUMNS;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_PACKAGE;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.CLASS_NAME;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_ENTRIES;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_KEY_REF;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON_NORMALIZED;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_TITLE;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.ENABLED;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.ICON;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.INTENT_ACTION;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_CLASS;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_PACKAGE;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.PAYLOAD;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.PAYLOAD_TYPE;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.SCREEN_TITLE;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX;
+import static com.android.settings.intelligence.search.SearchFeatureProvider.DEBUG;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.os.AsyncTask;
+import android.provider.SearchIndexablesContract;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto;
+import com.android.settings.intelligence.overlay.FeatureFactory;
+import com.android.settings.intelligence.search.sitemap.SiteMapPair;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Consumes the SearchIndexableProvider content providers.
+ * Updates the Resource, Raw Data and non-indexable data for Search.
+ *
+ * TODO(b/33577327) this class needs to be refactored by moving most of its methods into controllers
+ */
+public class DatabaseIndexingManager {
+
+    private static final String TAG = "DatabaseIndexingManager";
+
+    @VisibleForTesting
+    final AtomicBoolean mIsIndexingComplete = new AtomicBoolean(false);
+
+    private PreIndexDataCollector mCollector;
+    private IndexDataConverter mConverter;
+
+    private Context mContext;
+
+    public DatabaseIndexingManager(Context context) {
+        mContext = context;
+    }
+
+    public boolean isIndexingComplete() {
+        return mIsIndexingComplete.get();
+    }
+
+    public void indexDatabase(IndexingCallback callback) {
+        IndexingTask task = new IndexingTask(callback);
+        task.execute();
+    }
+
+    /**
+     * Accumulate all data and non-indexable keys from each of the content-providers.
+     * Only the first indexing for the default language gets static search results - subsequent
+     * calls will only gather non-indexable keys.
+     */
+    public void performIndexing() {
+        final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);
+        final List<ResolveInfo> providers =
+                mContext.getPackageManager().queryIntentContentProviders(intent, 0);
+
+        final boolean isFullIndex = IndexDatabaseHelper.isFullIndex(mContext, providers);
+
+        if (isFullIndex) {
+            rebuildDatabase();
+        }
+
+        PreIndexData indexData = getIndexDataFromProviders(providers, isFullIndex);
+
+        final long updateDatabaseStartTime = System.currentTimeMillis();
+        updateDatabase(indexData, isFullIndex);
+        IndexDatabaseHelper.setIndexed(mContext, providers);
+        if (DEBUG) {
+            final long updateDatabaseTime = System.currentTimeMillis() - updateDatabaseStartTime;
+            Log.d(TAG, "performIndexing updateDatabase took time: " + updateDatabaseTime);
+        }
+    }
+
+    @VisibleForTesting
+    PreIndexData getIndexDataFromProviders(List<ResolveInfo> providers, boolean isFullIndex) {
+        if (mCollector == null) {
+            mCollector = new PreIndexDataCollector(mContext);
+        }
+        return mCollector.collectIndexableData(providers, isFullIndex);
+    }
+
+    /**
+     * Drop the currently stored database, and clear the flags which mark the database as indexed.
+     */
+    private void rebuildDatabase() {
+        // Drop the database when the locale or build has changed. This eliminates rows which are
+        // dynamically inserted in the old language, or deprecated settings.
+        final SQLiteDatabase db = getWritableDatabase();
+        IndexDatabaseHelper.getInstance(mContext).reconstruct(db);
+    }
+
+    /**
+     * Adds new data to the database and verifies the correctness of the ENABLED column.
+     * First, the data to be updated and all non-indexable keys are copied locally.
+     * Then all new data to be added is inserted.
+     * Then search results are verified to have the correct value of enabled.
+     * Finally, we record that the locale has been indexed.
+     *
+     * @param isFullIndex true the database needs to be rebuilt.
+     */
+    @VisibleForTesting
+    void updateDatabase(PreIndexData preIndexData, boolean isFullIndex) {
+        final Map<String, Set<String>> nonIndexableKeys = preIndexData.getNonIndexableKeys();
+
+        final SQLiteDatabase database = getWritableDatabase();
+        if (database == null) {
+            Log.w(TAG, "Cannot indexDatabase Index as I cannot get a writable database");
+            return;
+        }
+
+        try {
+            database.beginTransaction();
+
+            // Convert all Pre-index data to Index data and and insert to db.
+            final List<IndexData> indexData = getIndexData(preIndexData);
+            insertIndexData(database, indexData);
+            insertSiteMapData(database, getSiteMapPairs(indexData, preIndexData.getSiteMapPairs()));
+
+            // Only check for non-indexable key updates after initial index.
+            // Enabled state with non-indexable keys is checked when items are first inserted.
+            if (!isFullIndex) {
+                updateDataInDatabase(database, nonIndexableKeys);
+            }
+
+            database.setTransactionSuccessful();
+        } finally {
+            database.endTransaction();
+        }
+    }
+
+    private List<IndexData> getIndexData(PreIndexData data) {
+        if (mConverter == null) {
+            mConverter = new IndexDataConverter(mContext);
+        }
+        return mConverter.convertPreIndexDataToIndexData(data);
+    }
+
+    private List<SiteMapPair> getSiteMapPairs(List<IndexData> indexData,
+            List<Pair<String, String>> siteMapClassNames) {
+        if (mConverter == null) {
+            mConverter = new IndexDataConverter(mContext);
+        }
+        return mConverter.convertSiteMapPairs(indexData, siteMapClassNames);
+    }
+
+    private void insertSiteMapData(SQLiteDatabase database, List<SiteMapPair> siteMapPairs) {
+        if (siteMapPairs == null) {
+            return;
+        }
+        for (SiteMapPair pair : siteMapPairs) {
+            database.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_SITE_MAP,
+                    null /* nullColumnHack */, pair.toContentValue());
+        }
+    }
+
+    /**
+     * Inserts all of the entries in {@param indexData} into the {@param database}
+     * as Search Data and as part of the Information Hierarchy.
+     */
+    private void insertIndexData(SQLiteDatabase database, List<IndexData> indexData) {
+        ContentValues values;
+
+        for (IndexData dataRow : indexData) {
+            if (TextUtils.isEmpty(dataRow.normalizedTitle)) {
+                continue;
+            }
+
+            values = new ContentValues();
+            values.put(DATA_TITLE, dataRow.updatedTitle);
+            values.put(DATA_TITLE_NORMALIZED, dataRow.normalizedTitle);
+            values.put(DATA_SUMMARY_ON, dataRow.updatedSummaryOn);
+            values.put(DATA_SUMMARY_ON_NORMALIZED, dataRow.normalizedSummaryOn);
+            values.put(DATA_ENTRIES, dataRow.entries);
+            values.put(DATA_KEYWORDS, dataRow.spaceDelimitedKeywords);
+            values.put(DATA_PACKAGE, dataRow.packageName);
+            values.put(CLASS_NAME, dataRow.className);
+            values.put(SCREEN_TITLE, dataRow.screenTitle);
+            values.put(INTENT_ACTION, dataRow.intentAction);
+            values.put(INTENT_TARGET_PACKAGE, dataRow.intentTargetPackage);
+            values.put(INTENT_TARGET_CLASS, dataRow.intentTargetClass);
+            values.put(ICON, dataRow.iconResId);
+            values.put(ENABLED, dataRow.enabled);
+            values.put(DATA_KEY_REF, dataRow.key);
+            values.put(PAYLOAD_TYPE, dataRow.payloadType);
+            values.put(PAYLOAD, dataRow.payload);
+
+            database.replaceOrThrow(TABLE_PREFS_INDEX, null, values);
+        }
+    }
+
+    /**
+     * Upholds the validity of enabled data for the user.
+     * All rows which are enabled but are now flagged with non-indexable keys will become disabled.
+     * All rows which are disabled but no longer a non-indexable key will become enabled.
+     *
+     * @param database         The database to validate.
+     * @param nonIndexableKeys A map between package name and the set of non-indexable keys for it.
+     */
+    @VisibleForTesting
+    void updateDataInDatabase(SQLiteDatabase database,
+            Map<String, Set<String>> nonIndexableKeys) {
+        final String whereEnabled = ENABLED + " = 1";
+        final String whereDisabled = ENABLED + " = 0";
+
+        final Cursor enabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
+                whereEnabled, null, null, null, null);
+
+        final ContentValues enabledToDisabledValue = new ContentValues();
+        enabledToDisabledValue.put(ENABLED, 0);
+
+        String packageName;
+        // TODO Refactor: Move these two loops into one method.
+        while (enabledResults.moveToNext()) {
+            packageName = enabledResults.getString(enabledResults.getColumnIndexOrThrow(
+                    IndexDatabaseHelper.IndexColumns.DATA_PACKAGE));
+            final String key = enabledResults.getString(enabledResults.getColumnIndexOrThrow(
+                    IndexDatabaseHelper.IndexColumns.DATA_KEY_REF));
+            final Set<String> packageKeys = nonIndexableKeys.get(packageName);
+
+            // The indexed item is set to Enabled but is now non-indexable
+            if (packageKeys != null && packageKeys.contains(key)) {
+                final String whereClause = getKeyWhereClause(key);
+                database.update(TABLE_PREFS_INDEX, enabledToDisabledValue, whereClause, null);
+            }
+        }
+        enabledResults.close();
+
+        final Cursor disabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
+                whereDisabled, null, null, null, null);
+
+        final ContentValues disabledToEnabledValue = new ContentValues();
+        disabledToEnabledValue.put(ENABLED, 1);
+
+        while (disabledResults.moveToNext()) {
+            packageName = disabledResults.getString(disabledResults.getColumnIndexOrThrow(
+                    IndexDatabaseHelper.IndexColumns.DATA_PACKAGE));
+
+            final String key = disabledResults.getString(disabledResults.getColumnIndexOrThrow(
+                    IndexDatabaseHelper.IndexColumns.DATA_KEY_REF));
+            final Set<String> packageKeys = nonIndexableKeys.get(packageName);
+
+            // The indexed item is set to Disabled but is no longer non-indexable.
+            // We do not enable keys when packageKeys is null because it means the keys came
+            // from an unrecognized package and therefore should not be surfaced as results.
+            if (packageKeys != null && !packageKeys.contains(key)) {
+                final String whereClause = getKeyWhereClause(key);
+                database.update(TABLE_PREFS_INDEX, disabledToEnabledValue, whereClause, null);
+            }
+        }
+        disabledResults.close();
+    }
+
+    private String getKeyWhereClause(String key) {
+        return IndexDatabaseHelper.IndexColumns.DATA_KEY_REF + " = \"" + key + "\"";
+    }
+
+    private SQLiteDatabase getWritableDatabase() {
+        try {
+            return IndexDatabaseHelper.getInstance(mContext).getWritableDatabase();
+        } catch (SQLiteException e) {
+            Log.e(TAG, "Cannot open writable database", e);
+            return null;
+        }
+    }
+
+    public class IndexingTask extends AsyncTask<Void, Void, Void> {
+
+        @VisibleForTesting
+        IndexingCallback mCallback;
+        private long mIndexStartTime;
+
+        public IndexingTask(IndexingCallback callback) {
+            mCallback = callback;
+        }
+
+        @Override
+        protected void onPreExecute() {
+            mIndexStartTime = System.currentTimeMillis();
+            mIsIndexingComplete.set(false);
+        }
+
+        @Override
+        protected Void doInBackground(Void... voids) {
+            performIndexing();
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void aVoid) {
+            int indexingTime = (int) (System.currentTimeMillis() - mIndexStartTime);
+            FeatureFactory.get(mContext).metricsFeatureProvider(mContext).logEvent(
+                    SettingsIntelligenceLogProto.SettingsIntelligenceEvent.INDEX_SEARCH,
+                    indexingTime);
+
+            mIsIndexingComplete.set(true);
+            if (mCallback != null) {
+                mCallback.onIndexingFinished();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/search/indexing/DatabaseIndexingUtils.java b/src/com/android/settings/intelligence/search/indexing/DatabaseIndexingUtils.java
new file mode 100644
index 0000000..534a20c
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/indexing/DatabaseIndexingUtils.java
@@ -0,0 +1,139 @@
+/*
+ * 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.indexing;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+
+/**
+ * Utility class for {@like DatabaseIndexingManager} to handle the mapping between Payloads
+ * and Preference controllers, and managing indexable classes.
+ */
+public class DatabaseIndexingUtils {
+
+    private static final String TAG = "DatabaseIndexingUtils";
+
+    // frameworks/base/proto/src/metrics_constants.proto#DASHBOARD_SEARCH_RESULTS
+    // Have to hardcode value here because we can't depend on internal framework constants.
+    public static final int DASHBOARD_SEARCH_RESULTS = 34;
+
+    /**
+     * Below are internal contract between Settings/SettingsIntelligence to launch a search result
+     * page.
+     */
+    public static final String EXTRA_SHOW_FRAGMENT = ":settings:show_fragment";
+    public static final String EXTRA_SOURCE_METRICS_CATEGORY = ":settings:source_metrics";
+    public static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key";
+    public static final String EXTRA_SHOW_FRAGMENT_TITLE = ":settings:show_fragment_title";
+    public static final String SEARCH_RESULT_TRAMPOLINE_ACTION =
+            "com.android.settings.SEARCH_RESULT_TRAMPOLINE";
+
+    /**
+     * Builds intent that launches the search destination as a sub-setting.
+     */
+    public static Intent buildSearchTrampolineIntent(Context context, String className, String key,
+            String screenTitle) {
+        final Intent intent = new Intent(SEARCH_RESULT_TRAMPOLINE_ACTION);
+        intent.putExtra(EXTRA_SHOW_FRAGMENT, className)
+                .putExtra(EXTRA_SHOW_FRAGMENT_TITLE, screenTitle)
+                .putExtra(EXTRA_SOURCE_METRICS_CATEGORY, DASHBOARD_SEARCH_RESULTS)
+                .putExtra(EXTRA_FRAGMENT_ARG_KEY, key);
+        return intent;
+    }
+
+    public static Intent buildDirectSearchResultIntent(String action, String targetPackage,
+            String targetClass, String key) {
+        final Intent intent = new Intent(action).putExtra(EXTRA_FRAGMENT_ARG_KEY, key);
+        if (!TextUtils.isEmpty(targetPackage) && !TextUtils.isEmpty(targetClass)) {
+            final ComponentName component = new ComponentName(targetPackage, targetClass);
+            intent.setComponent(component);
+        }
+        return intent;
+    }
+
+    // TODO REFACTOR (b/62807132) Add inline support with proper intents.
+//    /**
+//     * @param className which wil provide the map between from {@link Uri}s to
+//     *                  {@link PreferenceControllerMixin}
+//     * @return A map between {@link Uri}s and {@link PreferenceControllerMixin}s to get the
+// payload
+//     * types for Settings.
+//     */
+//    public static Map<String, PreferenceControllerMixin> getPreferenceControllerUriMap(
+//            String className, Context context) {
+//        if (context == null) {
+//            return null;
+//        }
+//
+//        final Class<?> clazz = getIndexableClass(className);
+//
+//        if (clazz == null) {
+//            Log.d(TAG, "SearchIndexableResource '" + className +
+//                    "' should implement the " + Indexable.class.getName() + " interface!");
+//            return null;
+//        }
+//
+//        // Will be non null only for a Local provider implementing a
+//        // SEARCH_INDEX_DATA_PROVIDER field
+//        final Indexable.SearchIndexProvider provider = getSearchIndexProvider(clazz);
+//
+//        List<AbstractPreferenceController> controllers =
+//                provider.getPreferenceControllers(context);
+//
+//        if (controllers == null) {
+//            return null;
+//        }
+//
+//        ArrayMap<String, PreferenceControllerMixin> map = new ArrayMap<>();
+//
+//        for (AbstractPreferenceController controller : controllers) {
+//            if (controller instanceof PreferenceControllerMixin) {
+//                map.put(controller.getPreferenceKey(), (PreferenceControllerMixin) controller);
+//            } else {
+//                throw new IllegalStateException(controller.getClass().getName()
+//                        + " must implement " + PreferenceControllerMixin.class.getName());
+//            }
+//        }
+//
+//        return map;
+//    }
+//
+//    /**
+//     * @param uriMap Map between the {@link PreferenceControllerMixin} keys
+//     *               and the controllers themselves.
+//     * @param key    The look-up key
+//     * @return The Payload from the {@link PreferenceControllerMixin} specified by the key,
+//     * if it exists. Otherwise null.
+//     */
+//    public static ResultPayload getPayloadFromUriMap(Map<String, PreferenceControllerMixin>
+// uriMap,
+//            String key) {
+//        if (uriMap == null) {
+//            return null;
+//        }
+//
+//        PreferenceControllerMixin controller = uriMap.get(key);
+//        if (controller == null) {
+//            return null;
+//        }
+//
+//        return controller.getResultPayload();
+//    }
+
+}
diff --git a/src/com/android/settings/intelligence/search/indexing/IndexData.java b/src/com/android/settings/intelligence/search/indexing/IndexData.java
new file mode 100644
index 0000000..d178344
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/indexing/IndexData.java
@@ -0,0 +1,308 @@
+/*
+ * 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.indexing;
+
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+
+import com.android.settings.intelligence.search.ResultPayload;
+import com.android.settings.intelligence.search.ResultPayloadUtils;
+
+import java.text.Normalizer;
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+/**
+ * Data class representing a single row in the Setting Search results database.
+ */
+public class IndexData {
+    /**
+     * This is different from intentTargetPackage.
+     *
+     * @see SearchIndexableData#iconResId
+     */
+    public final String packageName;
+    public final String locale;
+    public final String updatedTitle;
+    public final String normalizedTitle;
+    public final String updatedSummaryOn;
+    public final String normalizedSummaryOn;
+    public final String entries;
+    public final String className;
+    public final String childClassName;
+    public final String screenTitle;
+    public final int iconResId;
+    public final String spaceDelimitedKeywords;
+    public final String intentAction;
+    public final String intentTargetPackage;
+    public final String intentTargetClass;
+    public final boolean enabled;
+    public final String key;
+    public final int payloadType;
+    public final byte[] payload;
+
+    private static final String NON_BREAKING_HYPHEN = "\u2011";
+    private static final String EMPTY = "";
+    private static final String HYPHEN = "-";
+    private static final String SPACE = " ";
+    // Regex matching a comma, and any number of subsequent white spaces.
+    private static final String LIST_DELIMITERS = "[,]\\s*";
+
+    private static final Pattern REMOVE_DIACRITICALS_PATTERN
+            = Pattern.compile("\\p{InCombiningDiacriticalMarks}+");
+
+    private IndexData(Builder builder) {
+        locale = Locale.getDefault().toString();
+        updatedTitle = normalizeHyphen(builder.mTitle);
+        updatedSummaryOn = normalizeHyphen(builder.mSummaryOn);
+        if (Locale.JAPAN.toString().equalsIgnoreCase(locale)) {
+            // Special case for JP. Convert charset to the same type for indexing purpose.
+            normalizedTitle = normalizeJapaneseString(builder.mTitle);
+            normalizedSummaryOn = normalizeJapaneseString(builder.mSummaryOn);
+        } else {
+            normalizedTitle = normalizeString(builder.mTitle);
+            normalizedSummaryOn = normalizeString(builder.mSummaryOn);
+        }
+        entries = builder.mEntries;
+        className = builder.mClassName;
+        childClassName = builder.mChildClassName;
+        screenTitle = builder.mScreenTitle;
+        iconResId = builder.mIconResId;
+        spaceDelimitedKeywords = normalizeKeywords(builder.mKeywords);
+        intentAction = builder.mIntentAction;
+        packageName = builder.mPackageName;
+        intentTargetPackage = builder.mIntentTargetPackage;
+        intentTargetClass = builder.mIntentTargetClass;
+        enabled = builder.mEnabled;
+        key = builder.mKey;
+        payloadType = builder.mPayloadType;
+        payload = builder.mPayload != null ? ResultPayloadUtils.marshall(builder.mPayload)
+                : null;
+    }
+
+    @Override
+    public String toString() {
+        return new StringBuilder(updatedTitle)
+                .append(": ")
+                .append(updatedSummaryOn)
+                .toString();
+    }
+
+    /**
+     * In the list of keywords, replace the comma and all subsequent whitespace with a single space.
+     */
+    public static String normalizeKeywords(String input) {
+        return (input != null) ? input.replaceAll(LIST_DELIMITERS, SPACE) : EMPTY;
+    }
+
+    /**
+     * @return {@param input} where all non-standard hyphens are replaced by normal hyphens.
+     */
+    public static String normalizeHyphen(String input) {
+        return (input != null) ? input.replaceAll(NON_BREAKING_HYPHEN, HYPHEN) : EMPTY;
+    }
+
+    /**
+     * @return {@param input} with all hyphens removed, and all letters lower case.
+     */
+    public static String normalizeString(String input) {
+        final String normalizedHypen = normalizeHyphen(input);
+        final String nohyphen = (input != null) ? normalizedHypen.replaceAll(HYPHEN, EMPTY) : EMPTY;
+        final String normalized = Normalizer.normalize(nohyphen, Normalizer.Form.NFD);
+
+        return REMOVE_DIACRITICALS_PATTERN.matcher(normalized).replaceAll("").toLowerCase();
+    }
+
+    public static String normalizeJapaneseString(String input) {
+        final String nohyphen = (input != null) ? input.replaceAll(HYPHEN, EMPTY) : EMPTY;
+        final String normalized = Normalizer.normalize(nohyphen, Normalizer.Form.NFKD);
+        final StringBuffer sb = new StringBuffer();
+        final int length = normalized.length();
+        for (int i = 0; i < length; i++) {
+            char c = normalized.charAt(i);
+            // Convert Hiragana to full-width Katakana
+            if (c >= '\u3041' && c <= '\u3096') {
+                sb.append((char) (c - '\u3041' + '\u30A1'));
+            } else {
+                sb.append(c);
+            }
+        }
+
+        return REMOVE_DIACRITICALS_PATTERN.matcher(sb.toString()).replaceAll("").toLowerCase();
+    }
+
+    public static class Builder {
+        private String mTitle;
+        private String mSummaryOn;
+        private String mEntries;
+        private String mClassName;
+        private String mChildClassName;
+        private String mScreenTitle;
+        private String mPackageName;
+        private int mIconResId;
+        private String mKeywords;
+        private String mIntentAction;
+        private String mIntentTargetPackage;
+        private String mIntentTargetClass;
+        private boolean mEnabled;
+        private String mKey;
+        @ResultPayload.PayloadType
+        private int mPayloadType;
+        private ResultPayload mPayload;
+
+        @Override
+        public String toString() {
+            return "IndexData.Builder {"
+                    + "title: " + mTitle + ","
+                    + "package: " + mPackageName
+                    + "}";
+        }
+
+        public Builder setTitle(String title) {
+            mTitle = title;
+            return this;
+        }
+
+        public String getKey() {
+            return mKey;
+        }
+
+        public Builder setSummaryOn(String summaryOn) {
+            mSummaryOn = summaryOn;
+            return this;
+        }
+
+        public Builder setEntries(String entries) {
+            mEntries = entries;
+            return this;
+        }
+
+        public Builder setClassName(String className) {
+            mClassName = className;
+            return this;
+        }
+
+        public Builder setChildClassName(String childClassName) {
+            mChildClassName = childClassName;
+            return this;
+        }
+
+        public Builder setScreenTitle(String screenTitle) {
+            mScreenTitle = screenTitle;
+            return this;
+        }
+
+        public Builder setPackageName(String packageName) {
+            mPackageName = packageName;
+            return this;
+        }
+
+        public Builder setIconResId(int iconResId) {
+            mIconResId = iconResId;
+            return this;
+        }
+
+        public Builder setKeywords(String keywords) {
+            mKeywords = keywords;
+            return this;
+        }
+
+        public Builder setIntentAction(String intentAction) {
+            mIntentAction = intentAction;
+            return this;
+        }
+
+        public Builder setIntentTargetPackage(String intentTargetPackage) {
+            mIntentTargetPackage = intentTargetPackage;
+            return this;
+        }
+
+        public Builder setIntentTargetClass(String intentTargetClass) {
+            mIntentTargetClass = intentTargetClass;
+            return this;
+        }
+
+        public Builder setEnabled(boolean enabled) {
+            mEnabled = enabled;
+            return this;
+        }
+
+        public Builder setKey(String key) {
+            mKey = key;
+            return this;
+        }
+
+        public Builder setPayload(ResultPayload payload) {
+            mPayload = payload;
+
+            if (mPayload != null) {
+                setPayloadType(mPayload.getType());
+            }
+            return this;
+        }
+
+        /**
+         * Payload type is added when a Payload is added to the Builder in {setPayload}
+         *
+         * @param payloadType PayloadType
+         * @return The Builder
+         */
+        private Builder setPayloadType(@ResultPayload.PayloadType int payloadType) {
+            mPayloadType = payloadType;
+            return this;
+        }
+
+        /**
+         * Adds intent to inline payloads, or creates an Intent Payload as a fallback if the
+         * payload is null.
+         */
+        private void setIntent(Context context) {
+            if (mPayload != null) {
+                return;
+            }
+            final Intent intent = buildIntent(context);
+            mPayload = new ResultPayload(intent);
+            mPayloadType = ResultPayload.PayloadType.INTENT;
+        }
+
+        /**
+         * Adds Intent payload to builder.
+         */
+        private Intent buildIntent(Context context) {
+            final Intent intent;
+
+            // TODO REFACTOR (b/62807132) With inline results re-add proper intent support
+            boolean isEmptyIntentAction = TextUtils.isEmpty(mIntentAction);
+            if (isEmptyIntentAction) {
+                // No intent action is set, or the intent action is for a sub-setting.
+                intent = DatabaseIndexingUtils.buildSearchTrampolineIntent(context, mClassName,
+                        mKey, mScreenTitle);
+            } else {
+                intent = DatabaseIndexingUtils.buildDirectSearchResultIntent(mIntentAction,
+                        mIntentTargetPackage, mIntentTargetClass, mKey);
+            }
+            return intent;
+        }
+
+        public IndexData build(Context context) {
+            setIntent(context);
+            return new IndexData(this);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/search/indexing/IndexDataConverter.java b/src/com/android/settings/intelligence/search/indexing/IndexDataConverter.java
new file mode 100644
index 0000000..167019b
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/indexing/IndexDataConverter.java
@@ -0,0 +1,360 @@
+/*
+ * 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.indexing;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.os.AsyncTask;
+import android.provider.SearchIndexableData;
+import android.provider.SearchIndexableResource;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Xml;
+
+import com.android.settings.intelligence.search.ResultPayload;
+import com.android.settings.intelligence.search.SearchFeatureProvider;
+import com.android.settings.intelligence.search.SearchIndexableRaw;
+import com.android.settings.intelligence.search.sitemap.SiteMapPair;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * Helper class to convert {@link PreIndexData} to {@link IndexData}.
+ */
+public class IndexDataConverter {
+
+    private static final String TAG = "IndexDataConverter";
+
+    private static final String NODE_NAME_PREFERENCE_SCREEN = "PreferenceScreen";
+    private static final String NODE_NAME_CHECK_BOX_PREFERENCE = "CheckBoxPreference";
+    private static final String NODE_NAME_LIST_PREFERENCE = "ListPreference";
+    private static final List<String> SKIP_NODES = Arrays.asList("intent", "extra");
+
+    private final Context mContext;
+
+    public IndexDataConverter(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Return the collection of {@param preIndexData} converted into {@link IndexData}.
+     *
+     * @param preIndexData a collection of {@link SearchIndexableResource},
+     *                     {@link SearchIndexableRaw} and non-indexable keys.
+     */
+    public List<IndexData> convertPreIndexDataToIndexData(PreIndexData preIndexData) {
+        final long startConversion = System.currentTimeMillis();
+        final List<SearchIndexableData> indexableData = preIndexData.getDataToUpdate();
+        final Map<String, Set<String>> nonIndexableKeys = preIndexData.getNonIndexableKeys();
+        final List<IndexData> indexData = new ArrayList<>();
+
+        for (SearchIndexableData data : indexableData) {
+            if (data instanceof SearchIndexableRaw) {
+                final SearchIndexableRaw rawData = (SearchIndexableRaw) data;
+                final Set<String> rawNonIndexableKeys = nonIndexableKeys.get(
+                        rawData.intentTargetPackage);
+                final IndexData convertedRaw = convertRaw(mContext, rawData, rawNonIndexableKeys);
+                if (convertedRaw != null) {
+                    indexData.add(convertedRaw);
+                }
+            } else if (data instanceof SearchIndexableResource) {
+                final SearchIndexableResource sir = (SearchIndexableResource) data;
+                final Set<String> resourceNonIndexableKeys =
+                        getNonIndexableKeysForResource(nonIndexableKeys, sir.packageName);
+                final List<IndexData> resourceData = convertResource(sir, resourceNonIndexableKeys);
+                indexData.addAll(resourceData);
+            }
+        }
+
+        final long endConversion = System.currentTimeMillis();
+        Log.d(TAG, "Converting pre-index data to index data took: "
+                + (endConversion - startConversion));
+
+        return indexData;
+    }
+
+    /**
+     * Returns a full list of site map pairs based on metadata from all data sources.
+     *
+     * The content schema follows {@link IndexDatabaseHelper.Tables#TABLE_SITE_MAP}
+     */
+    public List<SiteMapPair> convertSiteMapPairs(List<IndexData> indexData,
+            List<Pair<String, String>> siteMapClassNames) {
+        final List<SiteMapPair> pairs = new ArrayList<>();
+        if (indexData == null) {
+            return pairs;
+        }
+        // Step 1: loop indexData and build all static site map pairs.
+        final Map<String, String> classToTitleMap = new TreeMap<>();
+        for (IndexData row : indexData) {
+            if (TextUtils.isEmpty(row.className)) {
+                continue;
+            }
+            // Build a map of [class, title] for the next step.
+            classToTitleMap.put(row.className, row.screenTitle);
+            if (!TextUtils.isEmpty(row.childClassName)) {
+                pairs.add(new SiteMapPair(row.className, row.screenTitle,
+                        row.childClassName, row.updatedTitle));
+            }
+        }
+        // Step 2: Extend the sitemap pairs by adding dynamic pairs provided by
+        // SearchIndexableProvider. The provider only tells us class name so we need to finish
+        // the mapping by looking up display title for each class.
+        for (Pair<String, String> pair : siteMapClassNames) {
+            final String parentName = classToTitleMap.get(pair.first);
+            final String childName = classToTitleMap.get(pair.second);
+            if (TextUtils.isEmpty(parentName) || TextUtils.isEmpty(childName)) {
+                Log.w(TAG, "Cannot build sitemap pair for incomplete names "
+                        + pair + parentName + childName);
+            } else {
+                pairs.add(new SiteMapPair(pair.first, parentName, pair.second, childName));
+            }
+        }
+        // Done
+        return pairs;
+    }
+
+    /**
+     * Return the conversion of {@link SearchIndexableRaw} to {@link IndexData}.
+     * The fields of {@link SearchIndexableRaw} are a subset of {@link IndexData},
+     * and there is some data sanitization in the conversion.
+     */
+    @Nullable
+    private IndexData convertRaw(Context context, SearchIndexableRaw raw,
+            Set<String> nonIndexableKeys) {
+        if (TextUtils.isEmpty(raw.key)) {
+            Log.w(TAG, "Skipping null key for raw indexable " + raw.packageName + "/" + raw.title);
+            return null;
+        }
+        // A row is enabled if it does not show up as an nonIndexableKey
+        boolean enabled = !(nonIndexableKeys != null && nonIndexableKeys.contains(raw.key));
+
+        final IndexData.Builder builder = new IndexData.Builder();
+        builder.setTitle(raw.title)
+                .setSummaryOn(raw.summaryOn)
+                .setEntries(raw.entries)
+                .setKeywords(raw.keywords)
+                .setClassName(raw.className)
+                .setScreenTitle(raw.screenTitle)
+                .setIconResId(raw.iconResId)
+                .setIntentAction(raw.intentAction)
+                .setIntentTargetPackage(raw.intentTargetPackage)
+                .setIntentTargetClass(raw.intentTargetClass)
+                .setEnabled(enabled)
+                .setPackageName(raw.packageName)
+                .setKey(raw.key);
+
+        return builder.build(context);
+    }
+
+    /**
+     * Return the conversion of the {@link SearchIndexableResource} to {@link IndexData}.
+     * Each of the elements in the xml layout attribute of {@param sir} is a candidate to be
+     * converted (including the header element).
+     *
+     * TODO (b/33577327) simplify this method.
+     */
+    private List<IndexData> convertResource(SearchIndexableResource sir,
+            Set<String> nonIndexableKeys) {
+        final Context context = sir.context;
+        XmlResourceParser parser = null;
+
+        List<IndexData> resourceIndexData = new ArrayList<>();
+        try {
+            parser = context.getResources().getXml(sir.xmlResId);
+
+            int type;
+            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                    && type != XmlPullParser.START_TAG) {
+                // Parse next until start tag is found
+            }
+
+            String nodeName = parser.getName();
+            if (!NODE_NAME_PREFERENCE_SCREEN.equals(nodeName)) {
+                throw new RuntimeException(
+                        "XML document must start with <PreferenceScreen> tag; found"
+                                + nodeName + " at " + parser.getPositionDescription());
+            }
+
+            final int outerDepth = parser.getDepth();
+            final AttributeSet attrs = Xml.asAttributeSet(parser);
+
+            final String screenTitle = XmlParserUtils.getDataTitle(context, attrs);
+            final String headerKey = XmlParserUtils.getDataKey(context, attrs);
+
+            String title;
+            String key;
+            String headerTitle;
+            String summary;
+            String headerSummary;
+            String keywords;
+            String headerKeywords;
+            String childFragment;
+            @DrawableRes int iconResId;
+            ResultPayload payload;
+            boolean enabled;
+
+            // TODO REFACTOR (b/62807132) Add proper inline support
+//            Map<String, PreferenceControllerMixin> controllerUriMap = null;
+//
+//            if (fragmentName != null) {
+//                controllerUriMap = DatabaseIndexingUtils
+//                        .getPreferenceControllerUriMap(fragmentName, context);
+//            }
+
+            headerTitle = XmlParserUtils.getDataTitle(context, attrs);
+            headerSummary = XmlParserUtils.getDataSummary(context, attrs);
+            headerKeywords = XmlParserUtils.getDataKeywords(context, attrs);
+            enabled = !nonIndexableKeys.contains(headerKey);
+            // TODO: Set payload type for header results
+            IndexData.Builder headerBuilder = new IndexData.Builder();
+            headerBuilder.setTitle(headerTitle)
+                    .setSummaryOn(headerSummary)
+                    .setScreenTitle(screenTitle)
+                    .setKeywords(headerKeywords)
+                    .setClassName(sir.className)
+                    .setPackageName(sir.packageName)
+                    .setIntentAction(sir.intentAction)
+                    .setIntentTargetPackage(sir.intentTargetPackage)
+                    .setIntentTargetClass(sir.intentTargetClass)
+                    .setEnabled(enabled)
+                    .setKey(headerKey);
+
+            // Flag for XML headers which a child element's title.
+            boolean isHeaderUnique = true;
+            IndexData.Builder builder;
+
+            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                    continue;
+                }
+
+                nodeName = parser.getName();
+                if (SKIP_NODES.contains(nodeName)) {
+                    if (SearchFeatureProvider.DEBUG) {
+                        Log.d(TAG, nodeName + " is not a valid entity to index, skip.");
+                    }
+                    continue;
+                }
+
+                title = XmlParserUtils.getDataTitle(context, attrs);
+                key = XmlParserUtils.getDataKey(context, attrs);
+                enabled = !nonIndexableKeys.contains(key);
+                keywords = XmlParserUtils.getDataKeywords(context, attrs);
+                iconResId = XmlParserUtils.getDataIcon(context, attrs);
+
+                if (isHeaderUnique && TextUtils.equals(headerTitle, title)) {
+                    isHeaderUnique = false;
+                }
+
+                builder = new IndexData.Builder();
+                builder.setTitle(title)
+                        .setKeywords(keywords)
+                        .setClassName(sir.className)
+                        .setScreenTitle(screenTitle)
+                        .setIconResId(iconResId)
+                        .setPackageName(sir.packageName)
+                        .setIntentAction(sir.intentAction)
+                        .setIntentTargetPackage(sir.intentTargetPackage)
+                        .setIntentTargetClass(sir.intentTargetClass)
+                        .setEnabled(enabled)
+                        .setKey(key);
+
+                if (!nodeName.equals(NODE_NAME_CHECK_BOX_PREFERENCE)) {
+                    summary = XmlParserUtils.getDataSummary(context, attrs);
+
+                    String entries = null;
+
+                    if (nodeName.endsWith(NODE_NAME_LIST_PREFERENCE)) {
+                        entries = XmlParserUtils.getDataEntries(context, attrs);
+                    }
+
+                    // TODO (b/62254931) index primitives instead of payload
+                    // TODO (b/62807132) Add proper inline support
+                    //payload = DatabaseIndexingUtils.getPayloadFromUriMap(controllerUriMap, key);
+                    childFragment = XmlParserUtils.getDataChildFragment(context, attrs);
+
+                    builder.setSummaryOn(summary)
+                            .setEntries(entries)
+                            .setChildClassName(childFragment);
+                    tryAddIndexDataToList(resourceIndexData, builder);
+                } else {
+                    // TODO (b/33577327) We removed summary off here. We should check if we can
+                    // merge this 'else' section with the one above. Put a break point to
+                    // investigate.
+                    String summaryOn = XmlParserUtils.getDataSummaryOn(context, attrs);
+
+                    if (TextUtils.isEmpty(summaryOn)) {
+                        summaryOn = XmlParserUtils.getDataSummary(context, attrs);
+                    }
+
+                    builder.setSummaryOn(summaryOn);
+
+                    tryAddIndexDataToList(resourceIndexData, builder);
+                }
+            }
+
+            // The xml header's title does not match the title of one of the child settings.
+            if (isHeaderUnique) {
+                tryAddIndexDataToList(resourceIndexData, headerBuilder);
+            }
+        } catch (XmlPullParserException e) {
+            Log.w(TAG, "XML Error parsing PreferenceScreen: " + sir.className, e);
+        } catch (IOException e) {
+            Log.w(TAG, "IO Error parsing PreferenceScreen: " + sir.className, e);
+        } catch (Resources.NotFoundException e) {
+            Log.w(TAG, "Resoucre not found error parsing PreferenceScreen: " + sir.className, e);
+        } finally {
+            if (parser != null) {
+                parser.close();
+            }
+        }
+        return resourceIndexData;
+    }
+
+    private void tryAddIndexDataToList(List<IndexData> list, IndexData.Builder data) {
+        if (!TextUtils.isEmpty(data.getKey())) {
+            list.add(data.build(mContext));
+        } else {
+            Log.w(TAG, "Skipping index for null-key item " + data);
+        }
+    }
+
+    private Set<String> getNonIndexableKeysForResource(Map<String, Set<String>> nonIndexableKeys,
+            String packageName) {
+        return nonIndexableKeys.containsKey(packageName)
+                ? nonIndexableKeys.get(packageName)
+                : new HashSet<String>();
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/indexing/IndexDatabaseHelper.java b/src/com/android/settings/intelligence/search/indexing/IndexDatabaseHelper.java
new file mode 100644
index 0000000..4bec50e
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/indexing/IndexDatabaseHelper.java
@@ -0,0 +1,328 @@
+/*
+ * 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.indexing;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.os.Build;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.List;
+import java.util.Locale;
+
+public class IndexDatabaseHelper extends SQLiteOpenHelper {
+
+    private static final String TAG = "IndexDatabaseHelper";
+
+    private static final String DATABASE_NAME = "search_index.db";
+    private static final int DATABASE_VERSION = 119;
+
+    @VisibleForTesting
+    static final String SHARED_PREFS_TAG = "indexing_manager";
+
+    private static final String PREF_KEY_INDEXED_PROVIDERS = "indexed_providers";
+
+    public interface Tables {
+        String TABLE_PREFS_INDEX = "prefs_index";
+        String TABLE_SITE_MAP = "site_map";
+        String TABLE_META_INDEX = "meta_index";
+        String TABLE_SAVED_QUERIES = "saved_queries";
+    }
+
+    public interface IndexColumns {
+        String DATA_TITLE = "data_title";
+        String DATA_TITLE_NORMALIZED = "data_title_normalized";
+        String DATA_SUMMARY_ON = "data_summary_on";
+        String DATA_SUMMARY_ON_NORMALIZED = "data_summary_on_normalized";
+        String DATA_SUMMARY_OFF = "data_summary_off";
+        String DATA_SUMMARY_OFF_NORMALIZED = "data_summary_off_normalized";
+        String DATA_ENTRIES = "data_entries";
+        String DATA_KEYWORDS = "data_keywords";
+        String DATA_PACKAGE = "package";
+        String CLASS_NAME = "class_name";
+        String SCREEN_TITLE = "screen_title";
+        String INTENT_ACTION = "intent_action";
+        String INTENT_TARGET_PACKAGE = "intent_target_package";
+        String INTENT_TARGET_CLASS = "intent_target_class";
+        String ICON = "icon";
+        String ENABLED = "enabled";
+        String DATA_KEY_REF = "data_key_reference";
+        String PAYLOAD_TYPE = "payload_type";
+        String PAYLOAD = "payload";
+    }
+
+    public interface MetaColumns {
+        String BUILD = "build";
+    }
+
+    public interface SavedQueriesColumns {
+        String QUERY = "query";
+        String TIME_STAMP = "timestamp";
+    }
+
+    public interface SiteMapColumns {
+        String DOCID = "docid";
+        String PARENT_CLASS = "parent_class";
+        String CHILD_CLASS = "child_class";
+        String PARENT_TITLE = "parent_title";
+        String CHILD_TITLE = "child_title";
+    }
+
+    private static final String CREATE_INDEX_TABLE =
+            "CREATE VIRTUAL TABLE " + Tables.TABLE_PREFS_INDEX + " USING fts4" +
+                    "(" +
+                    IndexColumns.DATA_TITLE +
+                    ", " +
+                    IndexColumns.DATA_TITLE_NORMALIZED +
+                    ", " +
+                    IndexColumns.DATA_SUMMARY_ON +
+                    ", " +
+                    IndexColumns.DATA_SUMMARY_ON_NORMALIZED +
+                    ", " +
+                    IndexColumns.DATA_SUMMARY_OFF +
+                    ", " +
+                    IndexColumns.DATA_SUMMARY_OFF_NORMALIZED +
+                    ", " +
+                    IndexColumns.DATA_ENTRIES +
+                    ", " +
+                    IndexColumns.DATA_KEYWORDS +
+                    ", " +
+                    IndexColumns.DATA_PACKAGE +
+                    ", " +
+                    IndexColumns.SCREEN_TITLE +
+                    ", " +
+                    IndexColumns.CLASS_NAME +
+                    ", " +
+                    IndexColumns.ICON +
+                    ", " +
+                    IndexColumns.INTENT_ACTION +
+                    ", " +
+                    IndexColumns.INTENT_TARGET_PACKAGE +
+                    ", " +
+                    IndexColumns.INTENT_TARGET_CLASS +
+                    ", " +
+                    IndexColumns.ENABLED +
+                    ", " +
+                    IndexColumns.DATA_KEY_REF +
+                    ", " +
+                    IndexColumns.PAYLOAD_TYPE +
+                    ", " +
+                    IndexColumns.PAYLOAD +
+                    ");";
+
+    private static final String CREATE_META_TABLE =
+            "CREATE TABLE " + Tables.TABLE_META_INDEX +
+                    "(" +
+                    MetaColumns.BUILD + " VARCHAR(32) NOT NULL" +
+                    ")";
+
+    private static final String CREATE_SAVED_QUERIES_TABLE =
+            "CREATE TABLE " + Tables.TABLE_SAVED_QUERIES +
+                    "(" +
+                    SavedQueriesColumns.QUERY + " VARCHAR(64) NOT NULL" +
+                    ", " +
+                    SavedQueriesColumns.TIME_STAMP + " INTEGER" +
+                    ")";
+
+    private static final String CREATE_SITE_MAP_TABLE =
+            "CREATE VIRTUAL TABLE " + Tables.TABLE_SITE_MAP + " USING fts4" +
+                    "(" +
+                    SiteMapColumns.PARENT_CLASS +
+                    ", " +
+                    SiteMapColumns.CHILD_CLASS +
+                    ", " +
+                    SiteMapColumns.PARENT_TITLE +
+                    ", " +
+                    SiteMapColumns.CHILD_TITLE +
+                    ")";
+    private static final String INSERT_BUILD_VERSION =
+            "INSERT INTO " + Tables.TABLE_META_INDEX +
+                    " VALUES ('" + Build.VERSION.INCREMENTAL + "');";
+
+    private static final String SELECT_BUILD_VERSION =
+            "SELECT " + MetaColumns.BUILD + " FROM " + Tables.TABLE_META_INDEX + " LIMIT 1;";
+
+    private static IndexDatabaseHelper sSingleton;
+
+    private final Context mContext;
+
+    public static synchronized IndexDatabaseHelper getInstance(Context context) {
+        if (sSingleton == null) {
+            sSingleton = new IndexDatabaseHelper(context);
+        }
+        return sSingleton;
+    }
+
+    public IndexDatabaseHelper(Context context) {
+        super(context, DATABASE_NAME, null, DATABASE_VERSION);
+        mContext = context.getApplicationContext();
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        bootstrapDB(db);
+    }
+
+    private void bootstrapDB(SQLiteDatabase db) {
+        db.execSQL(CREATE_INDEX_TABLE);
+        db.execSQL(CREATE_META_TABLE);
+        db.execSQL(CREATE_SAVED_QUERIES_TABLE);
+        db.execSQL(CREATE_SITE_MAP_TABLE);
+        db.execSQL(INSERT_BUILD_VERSION);
+        Log.i(TAG, "Bootstrapped database");
+    }
+
+    @Override
+    public void onOpen(SQLiteDatabase db) {
+        super.onOpen(db);
+
+        Log.i(TAG, "Using schema version: " + db.getVersion());
+
+        if (!Build.VERSION.INCREMENTAL.equals(getBuildVersion(db))) {
+            Log.w(TAG, "Index needs to be rebuilt as build-version is not the same");
+            // We need to drop the tables and recreate them
+            reconstruct(db);
+        } else {
+            Log.i(TAG, "Index is fine");
+        }
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        if (oldVersion < DATABASE_VERSION) {
+            Log.w(TAG, "Detected schema version '" + oldVersion + "'. " +
+                    "Index needs to be rebuilt for schema version '" + newVersion + "'.");
+            // We need to drop the tables and recreate them
+            reconstruct(db);
+        }
+    }
+
+    @Override
+    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        Log.w(TAG, "Detected schema version '" + oldVersion + "'. " +
+                "Index needs to be rebuilt for schema version '" + newVersion + "'.");
+        // We need to drop the tables and recreate them
+        reconstruct(db);
+    }
+
+    public void reconstruct(SQLiteDatabase db) {
+        mContext.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE)
+                .edit()
+                .clear()
+                .commit();
+        dropTables(db);
+        bootstrapDB(db);
+    }
+
+    private String getBuildVersion(SQLiteDatabase db) {
+        String version = null;
+        Cursor cursor = null;
+        try {
+            cursor = db.rawQuery(SELECT_BUILD_VERSION, null);
+            if (cursor.moveToFirst()) {
+                version = cursor.getString(0);
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Cannot get build version from Index metadata");
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+        return version;
+    }
+
+    @VisibleForTesting
+    static String buildProviderVersionedNames(Context context, List<ResolveInfo> providers) {
+        // TODO Refactor update test to reflect version code change.
+        try {
+            StringBuilder sb = new StringBuilder();
+            for (ResolveInfo info : providers) {
+                String packageName = info.providerInfo.packageName;
+                PackageInfo packageInfo = context.getPackageManager().getPackageInfo(packageName,
+                        0 /* flags */);
+                sb.append(packageName)
+                        .append(':')
+                        .append(packageInfo.versionCode)
+                        .append(',');
+            }
+            // add SettingsIntelligence version as well.
+            sb.append(context.getPackageName())
+                    .append(':')
+                    .append(context.getPackageManager()
+                            .getPackageInfo(context.getPackageName(), 0 /* flags */).versionCode);
+            return sb.toString();
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.d(TAG, "Could not find package name in provider", e);
+        }
+        return "";
+    }
+
+    /**
+     * Set a flag that indicates the search database is fully indexed.
+     */
+    static void setIndexed(Context context, List<ResolveInfo> providers) {
+        final String localeStr = Locale.getDefault().toString();
+        final String fingerprint = Build.FINGERPRINT;
+        final String providerVersionedNames =
+                IndexDatabaseHelper.buildProviderVersionedNames(context, providers);
+        context.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE)
+                .edit()
+                .putBoolean(localeStr, true)
+                .putBoolean(fingerprint, true)
+                .putString(PREF_KEY_INDEXED_PROVIDERS, providerVersionedNames)
+                .apply();
+    }
+
+    /**
+     * Checks if the indexed data requires full index. The index data is out of date when:
+     * - Device language has changed
+     * - Device has taken an OTA.
+     * In both cases, the device requires a full index.
+     *
+     * @return true if a full index should be preformed.
+     */
+    static boolean isFullIndex(Context context, List<ResolveInfo> providers) {
+        final String localeStr = Locale.getDefault().toString();
+        final String fingerprint = Build.FINGERPRINT;
+        final String providerVersionedNames =
+                IndexDatabaseHelper.buildProviderVersionedNames(context, providers);
+        final SharedPreferences prefs = context
+                .getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE);
+
+        final boolean isIndexed = prefs.getBoolean(fingerprint, false)
+                && prefs.getBoolean(localeStr, false)
+                && TextUtils.equals(
+                prefs.getString(PREF_KEY_INDEXED_PROVIDERS, null), providerVersionedNames);
+        return !isIndexed;
+    }
+
+    private void dropTables(SQLiteDatabase db) {
+        db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_META_INDEX);
+        db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_PREFS_INDEX);
+        db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SAVED_QUERIES);
+        db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SITE_MAP);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/intelligence/TestConfig.java b/src/com/android/settings/intelligence/search/indexing/IndexingCallback.java
similarity index 73%
rename from tests/robotests/src/com/android/settings/intelligence/TestConfig.java
rename to src/com/android/settings/intelligence/search/indexing/IndexingCallback.java
index 44d238b..908db8f 100644
--- a/tests/robotests/src/com/android/settings/intelligence/TestConfig.java
+++ b/src/com/android/settings/intelligence/search/indexing/IndexingCallback.java
@@ -14,12 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.settings.intelligence;
+package com.android.settings.intelligence.search.indexing;
 
 /**
- * Constants for Robolectric config.
+ * Callback for Settings search indexing.
  */
-public class TestConfig {
-    public static final int SDK_VERSION = 26;
-    public static final String MANIFEST_PATH = "--default";
+public interface IndexingCallback {
+
+    /**
+     * Called when Indexing is finished.
+     */
+    void onIndexingFinished();
 }
diff --git a/src/com/android/settings/intelligence/search/indexing/PreIndexData.java b/src/com/android/settings/intelligence/search/indexing/PreIndexData.java
new file mode 100644
index 0000000..8f8ce07
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/indexing/PreIndexData.java
@@ -0,0 +1,72 @@
+/*
+ * 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.indexing;
+
+import android.provider.SearchIndexableData;
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+
+/**
+ * Holds Data sources for indexable data.
+ */
+public class PreIndexData {
+
+    private final List<SearchIndexableData> mDataToUpdate;
+    private final Map<String, Set<String>> mNonIndexableKeys;
+    private final List<Pair<String, String>> mSiteMapPairs;
+
+    public PreIndexData() {
+        mDataToUpdate = new ArrayList<>();
+        mNonIndexableKeys = new HashMap<>();
+        mSiteMapPairs = new ArrayList<>();
+    }
+
+    public Map<String, Set<String>> getNonIndexableKeys() {
+        return mNonIndexableKeys;
+    }
+
+    public List<SearchIndexableData> getDataToUpdate() {
+        return mDataToUpdate;
+    }
+
+    public List<Pair<String, String>> getSiteMapPairs() {
+        return mSiteMapPairs;
+    }
+
+    public void addNonIndexableKeysForAuthority(String authority, Set<String> keys) {
+        mNonIndexableKeys.put(authority, keys);
+    }
+
+    public void addDataToUpdate(List<? extends SearchIndexableData> data) {
+        mDataToUpdate.addAll(data);
+    }
+
+    public void addSiteMapPairs(List<Pair<String, String>> siteMapPairs) {
+        if (siteMapPairs == null) {
+            return;
+        }
+        mSiteMapPairs.addAll(siteMapPairs);
+    }
+
+}
diff --git a/src/com/android/settings/intelligence/search/indexing/PreIndexDataCollector.java b/src/com/android/settings/intelligence/search/indexing/PreIndexDataCollector.java
new file mode 100644
index 0000000..fbd3209
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/indexing/PreIndexDataCollector.java
@@ -0,0 +1,382 @@
+/*
+ * 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.indexing;
+
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_CLASS_NAME;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ENTRIES;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ICON_RESID;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_ACTION;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_CLASS;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEY;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEYWORDS;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SCREEN_TITLE;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_OFF;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_ON;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_TITLE;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_USER_ID;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_CLASS_NAME;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_ICON_RESID;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_ACTION;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID;
+
+import android.Manifest;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.SearchIndexableResource;
+import android.provider.SearchIndexablesContract;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.settings.intelligence.search.SearchFeatureProvider;
+import com.android.settings.intelligence.search.SearchIndexableRaw;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Collects all data from {@link android.provider.SearchIndexablesProvider} to be indexed.
+ */
+public class PreIndexDataCollector {
+
+    private static final String TAG = "IndexableDataCollector";
+
+    private static final List<String> EMPTY_LIST = Collections.emptyList();
+
+    private Context mContext;
+
+    private PreIndexData mIndexData;
+
+    public PreIndexDataCollector(Context context) {
+        mContext = context;
+    }
+
+    public PreIndexData collectIndexableData(List<ResolveInfo> providers, boolean isFullIndex) {
+        mIndexData = new PreIndexData();
+
+        for (final ResolveInfo info : providers) {
+            if (!isWellKnownProvider(info)) {
+                continue;
+            }
+            final String authority = info.providerInfo.authority;
+            final String packageName = info.providerInfo.packageName;
+
+            if (isFullIndex) {
+                addIndexablesFromRemoteProvider(packageName, authority);
+            }
+
+            final long nonIndexableStartTime = System.currentTimeMillis();
+            addNonIndexablesKeysFromRemoteProvider(packageName, authority);
+            if (SearchFeatureProvider.DEBUG) {
+                final long nonIndexableTime = System.currentTimeMillis() - nonIndexableStartTime;
+                Log.d(TAG, "performIndexing update non-indexable for package " + packageName
+                        + " took time: " + nonIndexableTime);
+            }
+        }
+
+        return mIndexData;
+    }
+
+    private void addIndexablesFromRemoteProvider(String packageName, String authority) {
+        try {
+            final Context context = mContext.createPackageContext(packageName, 0);
+
+            final Uri uriForResources = buildUriForXmlResources(authority);
+            mIndexData.addDataToUpdate(getIndexablesForXmlResourceUri(context, packageName,
+                    uriForResources, SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS));
+
+            final Uri uriForRawData = buildUriForRawData(authority);
+            mIndexData.addDataToUpdate(getIndexablesForRawDataUri(context, packageName,
+                    uriForRawData, SearchIndexablesContract.INDEXABLES_RAW_COLUMNS));
+
+            final Uri uriForSiteMap = buildUriForSiteMap(authority);
+            mIndexData.addSiteMapPairs(getSiteMapFromProvider(context, uriForSiteMap));
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(TAG, "Could not create context for " + packageName + ": "
+                    + Log.getStackTraceString(e));
+        }
+    }
+
+    @VisibleForTesting
+    List<SearchIndexableResource> getIndexablesForXmlResourceUri(Context packageContext,
+            String packageName, Uri uri, String[] projection) {
+
+        final ContentResolver resolver = packageContext.getContentResolver();
+        final Cursor cursor = resolver.query(uri, projection, null, null, null);
+        List<SearchIndexableResource> resources = new ArrayList<>();
+
+        if (cursor == null) {
+            Log.w(TAG, "Cannot add index data for Uri: " + uri.toString());
+            return resources;
+        }
+
+        try {
+            final int count = cursor.getCount();
+            if (count > 0) {
+                while (cursor.moveToNext()) {
+                    SearchIndexableResource sir = new SearchIndexableResource(packageContext);
+                    sir.packageName = packageName;
+                    sir.xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID);
+                    sir.className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME);
+                    sir.iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID);
+                    sir.intentAction = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION);
+                    sir.intentTargetPackage = cursor.getString(
+                            COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE);
+                    sir.intentTargetClass = cursor.getString(
+                            COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS);
+                    resources.add(sir);
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+        return resources;
+    }
+
+    private void addNonIndexablesKeysFromRemoteProvider(String packageName, String authority) {
+        final List<String> keys =
+                getNonIndexablesKeysFromRemoteProvider(packageName, authority);
+
+        if (keys != null && !keys.isEmpty()) {
+            Set<String> keySet = new ArraySet<>();
+            keySet.addAll(keys);
+            mIndexData.addNonIndexableKeysForAuthority(authority, keySet);
+        }
+    }
+
+    @VisibleForTesting
+    List<String> getNonIndexablesKeysFromRemoteProvider(String packageName,
+            String authority) {
+        try {
+            final Context packageContext = mContext.createPackageContext(packageName, 0);
+
+            final Uri uriForNonIndexableKeys = buildUriForNonIndexableKeys(authority);
+            return getNonIndexablesKeys(packageContext, uriForNonIndexableKeys,
+                    SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(TAG, "Could not create context for " + packageName + ": "
+                    + Log.getStackTraceString(e));
+            return EMPTY_LIST;
+        }
+    }
+
+    private Uri buildUriForXmlResources(String authority) {
+        return Uri.parse("content://" + authority + "/" +
+                SearchIndexablesContract.INDEXABLES_XML_RES_PATH);
+    }
+
+    private Uri buildUriForRawData(String authority) {
+        return Uri.parse("content://" + authority + "/" +
+                SearchIndexablesContract.INDEXABLES_RAW_PATH);
+    }
+
+    private Uri buildUriForNonIndexableKeys(String authority) {
+        return Uri.parse("content://" + authority + "/" +
+                SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH);
+    }
+
+    @VisibleForTesting
+    Uri buildUriForSiteMap(String authority) {
+        return Uri.parse("content://" + authority + "/settings/site_map_pairs");
+    }
+
+    @VisibleForTesting
+    List<SearchIndexableRaw> getIndexablesForRawDataUri(Context packageContext, String packageName,
+            Uri uri, String[] projection) {
+        final ContentResolver resolver = packageContext.getContentResolver();
+        final Cursor cursor = resolver.query(uri, projection, null, null, null);
+        List<SearchIndexableRaw> rawData = new ArrayList<>();
+
+        if (cursor == null) {
+            Log.w(TAG, "Cannot add index data for Uri: " + uri.toString());
+            return rawData;
+        }
+
+        try {
+            final int count = cursor.getCount();
+            if (count > 0) {
+                while (cursor.moveToNext()) {
+                    final String title = cursor.getString(COLUMN_INDEX_RAW_TITLE);
+                    final String summaryOn = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_ON);
+                    final String summaryOff = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_OFF);
+                    final String entries = cursor.getString(COLUMN_INDEX_RAW_ENTRIES);
+                    final String keywords = cursor.getString(COLUMN_INDEX_RAW_KEYWORDS);
+
+                    final String screenTitle = cursor.getString(COLUMN_INDEX_RAW_SCREEN_TITLE);
+
+                    final String className = cursor.getString(COLUMN_INDEX_RAW_CLASS_NAME);
+                    final int iconResId = cursor.getInt(COLUMN_INDEX_RAW_ICON_RESID);
+
+                    final String action = cursor.getString(COLUMN_INDEX_RAW_INTENT_ACTION);
+                    final String targetPackage = cursor.getString(
+                            COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE);
+                    final String targetClass = cursor.getString(
+                            COLUMN_INDEX_RAW_INTENT_TARGET_CLASS);
+
+                    final String key = cursor.getString(COLUMN_INDEX_RAW_KEY);
+                    final int userId = cursor.getInt(COLUMN_INDEX_RAW_USER_ID);
+
+                    SearchIndexableRaw data = new SearchIndexableRaw(packageContext);
+                    data.title = title;
+                    data.summaryOn = summaryOn;
+                    data.summaryOff = summaryOff;
+                    data.entries = entries;
+                    data.keywords = keywords;
+                    data.screenTitle = screenTitle;
+                    data.className = className;
+                    data.packageName = packageName;
+                    data.iconResId = iconResId;
+                    data.intentAction = action;
+                    data.intentTargetPackage = targetPackage;
+                    data.intentTargetClass = targetClass;
+                    data.key = key;
+                    data.userId = userId;
+
+                    rawData.add(data);
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+
+        return rawData;
+    }
+
+    @VisibleForTesting
+    List<Pair<String, String>> getSiteMapFromProvider(Context packageContext, Uri uri) {
+        final ContentResolver resolver = packageContext.getContentResolver();
+        final Cursor cursor = resolver.query(uri, null, null, null, null);
+        if (cursor == null) {
+            Log.d(TAG, "No site map information from " + packageContext.getPackageName());
+            return null;
+        }
+        final List<Pair<String, String>> siteMapPairs = new ArrayList<>();
+        try {
+            final int count = cursor.getCount();
+            if (count > 0) {
+                while (cursor.moveToNext()) {
+                    final String parentClass = cursor.getString(cursor.getColumnIndex(
+                            IndexDatabaseHelper.SiteMapColumns.PARENT_CLASS));
+                    final String childClass = cursor.getString(cursor.getColumnIndex(
+                            IndexDatabaseHelper.SiteMapColumns.CHILD_CLASS));
+                    if (TextUtils.isEmpty(parentClass)  || TextUtils.isEmpty(childClass)) {
+                        Log.w(TAG, "Incomplete site map pair: " + parentClass + "/" + childClass);
+                        continue;
+                    }
+                    siteMapPairs.add(Pair.create(parentClass, childClass));
+                }
+            }
+            return siteMapPairs;
+        } finally {
+            cursor.close();
+        }
+
+    }
+
+    private List<String> getNonIndexablesKeys(Context packageContext, Uri uri,
+            String[] projection) {
+
+        final ContentResolver resolver = packageContext.getContentResolver();
+        final Cursor cursor = resolver.query(uri, projection, null, null, null);
+        final List<String> result = new ArrayList<>();
+
+        if (cursor == null) {
+            Log.w(TAG, "Cannot add index data for Uri: " + uri.toString());
+            return result;
+        }
+
+        try {
+            final int count = cursor.getCount();
+            if (count > 0) {
+                while (cursor.moveToNext()) {
+                    final String key = cursor.getString(COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE);
+
+                    if (TextUtils.isEmpty(key) && Log.isLoggable(TAG, Log.VERBOSE)) {
+                        Log.v(TAG, "Empty non-indexable key from: "
+                                + packageContext.getPackageName());
+                        continue;
+                    }
+
+                    result.add(key);
+                }
+            }
+            return result;
+        } finally {
+            cursor.close();
+        }
+    }
+
+    /**
+     * Only allow a "well known" SearchIndexablesProvider. The provider should:
+     *
+     * - have read/write {@link Manifest.permission#READ_SEARCH_INDEXABLES}
+     * - be from a privileged package
+     */
+    @VisibleForTesting
+    boolean isWellKnownProvider(ResolveInfo info) {
+        final String authority = info.providerInfo.authority;
+        final String packageName = info.providerInfo.applicationInfo.packageName;
+
+        if (TextUtils.isEmpty(authority) || TextUtils.isEmpty(packageName)) {
+            return false;
+        }
+
+        final String readPermission = info.providerInfo.readPermission;
+        final String writePermission = info.providerInfo.writePermission;
+
+        if (TextUtils.isEmpty(readPermission) || TextUtils.isEmpty(writePermission)) {
+            return false;
+        }
+
+        if (!android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(readPermission) ||
+                !android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(writePermission)) {
+            return false;
+        }
+
+        return isPrivilegedPackage(packageName, mContext);
+    }
+
+    /**
+     * @return true if the {@param packageName} is privileged.
+     */
+    private boolean isPrivilegedPackage(String packageName, Context context) {
+        final PackageManager pm = context.getPackageManager();
+        try {
+            PackageInfo packInfo = pm.getPackageInfo(packageName, 0);
+            // TODO REFACTOR Changed privileged check
+            return ((packInfo.applicationInfo.flags
+                    & ApplicationInfo.FLAG_SYSTEM) != 0);
+        } catch (PackageManager.NameNotFoundException e) {
+            return false;
+        }
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/indexing/XmlParserUtils.java b/src/com/android/settings/intelligence/search/indexing/XmlParserUtils.java
new file mode 100644
index 0000000..6e7db64
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/indexing/XmlParserUtils.java
@@ -0,0 +1,142 @@
+/*
+ * 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.indexing;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+
+import com.android.settings.intelligence.R;
+
+/**
+ * Utility class to parse elements of XML preferences
+ */
+public class XmlParserUtils {
+
+    private static final String TAG = "XmlParserUtils";
+    private static final String NS_APP_RES_AUTO = "http://schemas.android.com/apk/res-auto";
+    private static final String ENTRIES_SEPARATOR = "|";
+
+    public static String getDataKey(Context context, AttributeSet attrs) {
+        return getData(context, attrs,
+                R.styleable.Preference,
+                R.styleable.Preference_android_key);
+    }
+
+    public static String getDataTitle(Context context, AttributeSet attrs) {
+        return getData(context, attrs,
+                R.styleable.Preference,
+                R.styleable.Preference_android_title);
+    }
+
+    public static String getDataSummary(Context context, AttributeSet attrs) {
+        return getData(context, attrs,
+                R.styleable.Preference,
+                R.styleable.Preference_android_summary);
+    }
+
+    public static String getDataSummaryOn(Context context, AttributeSet attrs) {
+        return getData(context, attrs,
+                R.styleable.CheckBoxPreference,
+                R.styleable.CheckBoxPreference_android_summaryOn);
+    }
+
+    public static String getDataSummaryOff(Context context, AttributeSet attrs) {
+        return getData(context, attrs,
+                R.styleable.CheckBoxPreference,
+                R.styleable.CheckBoxPreference_android_summaryOff);
+    }
+
+    public static String getDataEntries(Context context, AttributeSet attrs) {
+        return getDataEntries(context, attrs,
+                R.styleable.ListPreference,
+                R.styleable.ListPreference_android_entries);
+    }
+
+    public static String getDataKeywords(Context context, AttributeSet attrs) {
+        final String keywordRes = attrs.getAttributeValue(NS_APP_RES_AUTO, "keywords");
+        if (TextUtils.isEmpty(keywordRes)) {
+            return null;
+        }
+        // The format of keyword is either a string, or @ followed by int (@123456).
+        // When it's int, we need to look up the actual string from context.
+        if (!keywordRes.startsWith("@")) {
+            // It's a string.
+            return keywordRes;
+        } else {
+            // It's a resource
+            try  {
+                final int resValue = Integer.parseInt(keywordRes.substring(1));
+                return context.getString(resValue);
+            } catch (NumberFormatException e) {
+                Log.w(TAG, "Failed to parse keyword attribute, skipping " + keywordRes);
+                return null;
+            }
+        }
+    }
+
+    public static int getDataIcon(Context context, AttributeSet attrs) {
+        final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.Preference);
+        final int dataIcon = ta.getResourceId(R.styleable.Preference_android_icon, 0);
+        ta.recycle();
+        return dataIcon;
+    }
+
+    /**
+     * Returns the fragment name if this preference launches a child fragment.
+     */
+    public static String getDataChildFragment(Context context, AttributeSet attrs) {
+        return getData(context, attrs, R.styleable.Preference,
+                R.styleable.Preference_android_fragment);
+    }
+
+    @Nullable
+    private static String getData(Context context, AttributeSet set, int[] attrs, int resId) {
+        final TypedArray ta = context.obtainStyledAttributes(set, attrs);
+        String data = ta.getString(resId);
+        ta.recycle();
+        return data;
+    }
+
+
+    private static String getDataEntries(Context context, AttributeSet set, int[] attrs,
+            int resId) {
+        final TypedArray sa = context.obtainStyledAttributes(set, attrs);
+        final TypedValue tv = sa.peekValue(resId);
+        sa.recycle();
+        String[] data = null;
+        if (tv != null && tv.type == TypedValue.TYPE_REFERENCE) {
+            if (tv.resourceId != 0) {
+                data = context.getResources().getStringArray(tv.resourceId);
+            }
+        }
+        final int count = (data == null) ? 0 : data.length;
+        if (count == 0) {
+            return null;
+        }
+        final StringBuilder result = new StringBuilder();
+        for (int n = 0; n < count; n++) {
+            result.append(data[n]);
+            result.append(ENTRIES_SEPARATOR);
+        }
+        return result.toString();
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/query/AccessibilityServiceResultTask.java b/src/com/android/settings/intelligence/search/query/AccessibilityServiceResultTask.java
new file mode 100644
index 0000000..cd7b4ad
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/query/AccessibilityServiceResultTask.java
@@ -0,0 +1,119 @@
+/*
+ * 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.query;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.graphics.drawable.Drawable;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto;
+import com.android.settings.intelligence.search.ResultPayload;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.indexing.DatabaseIndexingUtils;
+import com.android.settings.intelligence.search.sitemap.SiteMapManager;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class AccessibilityServiceResultTask extends SearchQueryTask.QueryWorker {
+
+    public static final int QUERY_WORKER_ID = SettingsIntelligenceLogProto.SettingsIntelligenceEvent
+            .SEARCH_QUERY_ACCESSIBILITY_SERVICES;
+
+    private static final String ACCESSIBILITY_SETTINGS_CLASSNAME =
+            "com.android.settings.accessibility.AccessibilitySettings";
+    private static final int NAME_NO_MATCH = -1;
+
+    private final AccessibilityManager mAccessibilityManager;
+    private final PackageManager mPackageManager;
+
+    private List<String> mBreadcrumb;
+
+    public static SearchQueryTask newTask(Context context, SiteMapManager manager,
+            String query) {
+        return new SearchQueryTask(new AccessibilityServiceResultTask(context, manager, query));
+    }
+
+    public AccessibilityServiceResultTask(Context context, SiteMapManager mapManager,
+            String query) {
+        super(context, mapManager, query);
+        mPackageManager = context.getPackageManager();
+        mAccessibilityManager =
+                (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+    }
+
+    @Override
+    protected List<? extends SearchResult> query() {
+        final List<SearchResult> results = new ArrayList<>();
+        final List<AccessibilityServiceInfo> services = mAccessibilityManager
+                .getInstalledAccessibilityServiceList();
+        final String screenTitle = mContext.getString(R.string.accessibility_settings);
+        for (AccessibilityServiceInfo service : services) {
+            if (service == null) {
+                continue;
+            }
+            final ResolveInfo resolveInfo = service.getResolveInfo();
+            if (service.getResolveInfo() == null) {
+                continue;
+            }
+            final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+            final CharSequence title = resolveInfo.loadLabel(mPackageManager);
+            final int wordDiff = SearchQueryUtils.getWordDifference(title.toString(), mQuery);
+            if (wordDiff == NAME_NO_MATCH) {
+                continue;
+            }
+            final Drawable icon = serviceInfo.loadIcon(mPackageManager);
+            final String componentName = new ComponentName(serviceInfo.packageName,
+                    serviceInfo.name).flattenToString();
+            final Intent intent = DatabaseIndexingUtils.buildSearchTrampolineIntent(mContext,
+                    ACCESSIBILITY_SETTINGS_CLASSNAME, componentName, screenTitle);
+
+            results.add(new SearchResult.Builder()
+                    .setTitle(title)
+                    .addBreadcrumbs(getBreadCrumb())
+                    .setPayload(new ResultPayload(intent))
+                    .setRank(wordDiff)
+                    .setIcon(icon)
+                    .setDataKey(componentName)
+                    .build());
+        }
+        Collections.sort(results);
+        return results;
+    }
+
+    @Override
+    protected int getQueryWorkerId() {
+        return QUERY_WORKER_ID;
+    }
+
+    private List<String> getBreadCrumb() {
+        if (mBreadcrumb == null || mBreadcrumb.isEmpty()) {
+            mBreadcrumb = mSiteMapManager.buildBreadCrumb(
+                    mContext, ACCESSIBILITY_SETTINGS_CLASSNAME,
+                    mContext.getString(R.string.accessibility_settings));
+        }
+        return mBreadcrumb;
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/query/CursorToSearchResultConverter.java b/src/com/android/settings/intelligence/search/query/CursorToSearchResultConverter.java
new file mode 100644
index 0000000..dad9f14
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/query/CursorToSearchResultConverter.java
@@ -0,0 +1,215 @@
+/*
+ * 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.query;
+
+import static com.android.settings.intelligence.search.query.DatabaseResultTask.BASE_RANKS;
+import static com.android.settings.intelligence.search.SearchResult.TOP_RANK;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.os.BadParcelableException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.settings.intelligence.search.ResultPayload;
+import com.android.settings.intelligence.search.ResultPayloadUtils;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper;
+import com.android.settings.intelligence.search.sitemap.SiteMapManager;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Controller to Build search results from {@link Cursor} Objects.
+ *
+ * Each converted {@link Cursor} has the following fields:
+ * - String Title
+ * - String Summary
+ * - int rank
+ * - {@link Drawable} icon
+ * - {@link ResultPayload} payload
+ */
+public class CursorToSearchResultConverter {
+
+    private static final String TAG = "CursorConverter";
+
+    private final Context mContext;
+
+    private final int LONG_TITLE_LENGTH = 20;
+
+    private static final String[] whiteList = {
+            "main_toggle_wifi",
+            "main_toggle_bluetooth",
+            "main_toggle_bluetooth_obsolete",
+            "toggle_airplane",
+            "tether_settings",
+            "battery_saver",
+            "toggle_nfc",
+            "restrict_background",
+            "data_usage_enable",
+            "button_roaming_key",
+    };
+    private static final Set<String> prioritySettings = new HashSet(Arrays.asList(whiteList));
+
+
+    public CursorToSearchResultConverter(Context context) {
+        mContext = context;
+    }
+
+    public Set<SearchResult> convertCursor(Cursor cursorResults, int baseRank,
+            SiteMapManager siteMapManager) {
+        if (cursorResults == null) {
+            return null;
+        }
+        final Map<String, Context> contextMap = new HashMap<>();
+        final Set<SearchResult> results = new HashSet<>();
+
+        while (cursorResults.moveToNext()) {
+            SearchResult result = buildSingleSearchResultFromCursor(siteMapManager,
+                    contextMap, cursorResults, baseRank);
+            if (result != null) {
+                results.add(result);
+            }
+        }
+        return results;
+    }
+
+    public static ResultPayload getUnmarshalledPayload(byte[] marshalledPayload,
+            int payloadType) {
+        try {
+            switch (payloadType) {
+                case ResultPayload.PayloadType.INTENT:
+                    return ResultPayloadUtils.unmarshall(marshalledPayload,
+                            ResultPayload.CREATOR);
+                // TODO REFACTOR (b/62807132) Re-add inline
+//                case ResultPayload.PayloadType.INLINE_SWITCH:
+//                    return ResultPayloadUtils.unmarshall(marshalledPayload,
+//                            InlineSwitchPayload.CREATOR);
+//                case ResultPayload.PayloadType.INLINE_LIST:
+//                    return ResultPayloadUtils.unmarshall(marshalledPayload,
+//                            InlineListPayload.CREATOR);
+            }
+        } catch (BadParcelableException e) {
+            Log.w(TAG, "Error creating parcelable: " + e);
+        }
+        return null;
+    }
+
+    private SearchResult buildSingleSearchResultFromCursor(SiteMapManager siteMapManager,
+            Map<String, Context> contextMap, Cursor cursor, int baseRank) {
+        final String pkgName = cursor.getString(cursor.getColumnIndexOrThrow(
+                IndexDatabaseHelper.IndexColumns.DATA_PACKAGE));
+        final String title = cursor.getString(cursor.getColumnIndexOrThrow(
+                IndexDatabaseHelper.IndexColumns.DATA_TITLE));
+        final String summaryOn = cursor.getString(cursor.getColumnIndex(
+                IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON));
+        final String key = cursor.getString(cursor.getColumnIndexOrThrow(
+                IndexDatabaseHelper.IndexColumns.DATA_KEY_REF));
+        final String iconResStr = cursor.getString(cursor.getColumnIndexOrThrow(
+                IndexDatabaseHelper.IndexColumns.ICON));
+        final int payloadType = cursor.getInt(cursor.getColumnIndexOrThrow(
+                IndexDatabaseHelper.IndexColumns.PAYLOAD_TYPE));
+        final byte[] marshalledPayload = cursor.getBlob(cursor.getColumnIndexOrThrow(
+                IndexDatabaseHelper.IndexColumns.PAYLOAD));
+        final ResultPayload payload = getUnmarshalledPayload(marshalledPayload, payloadType);
+
+        final List<String> breadcrumbs = getBreadcrumbs(siteMapManager, cursor);
+        final int rank = getRank(title, baseRank, key);
+        final Drawable icon = getIconForPackage(contextMap, pkgName, iconResStr);
+
+        final SearchResult.Builder builder = new SearchResult.Builder()
+                .setDataKey(key)
+                .setTitle(title)
+                .setSummary(summaryOn)
+                .addBreadcrumbs(breadcrumbs)
+                .setRank(rank)
+                .setIcon(icon)
+                .setPayload(payload);
+        return builder.build();
+    }
+
+    private Drawable getIconForPackage(Map<String, Context> contextMap, String pkgName,
+            String iconResStr) {
+        if (TextUtils.isEmpty(pkgName)) {
+            return null;
+        }
+        final int iconId = TextUtils.isEmpty(iconResStr) ? 0 : Integer.parseInt(iconResStr);
+        if (iconId == 0) {
+            return null;
+        }
+        Context packageContext = contextMap.get(pkgName);
+        if (packageContext == null) {
+            try {
+                packageContext = mContext.createPackageContext(pkgName, 0);
+                contextMap.put(pkgName, packageContext);
+            } catch (PackageManager.NameNotFoundException e) {
+                Log.e(TAG, "Cannot create Context for package: " + pkgName);
+                return null;
+            }
+        }
+        try {
+            final Drawable drawable = packageContext.getDrawable(iconId);
+            Log.d(TAG, "Returning icon, id :" + iconId);
+            return drawable;
+        } catch (Resources.NotFoundException nfe) {
+            Log.w(TAG, "Cannot get icon, pkg/id :" + pkgName + "/" + iconId);
+            return null;
+        }
+    }
+
+    private List<String> getBreadcrumbs(SiteMapManager siteMapManager, Cursor cursor) {
+        final String screenTitle = cursor.getString(cursor.getColumnIndexOrThrow(
+                IndexDatabaseHelper.IndexColumns.SCREEN_TITLE));
+        final String screenClass = cursor.getString(cursor.getColumnIndexOrThrow(
+                IndexDatabaseHelper.IndexColumns.CLASS_NAME));
+        return siteMapManager == null ? null : siteMapManager.buildBreadCrumb(mContext,
+                screenClass, screenTitle);
+    }
+
+    /**
+     * Uses the breadcrumbs to determine the offset to the base rank.
+     * There are three checks
+     * A) If the result is prioritized and the highest base level
+     * B) If the query matches the highest level menu title
+     * C) If the query is longer than 20
+     *
+     * If the query matches A, set it to TOP_RANK
+     * If the query matches B, the offset is 0.
+     * If the query matches C, the offset is 1
+     *
+     * @param title    of the result.
+     * @param baseRank of the result. Lower if it's a better result.
+     */
+    private int getRank(String title, int baseRank, String key) {
+        // The result can only be prioritized if it is a top ranked result.
+        if (prioritySettings.contains(key) && baseRank < BASE_RANKS[1]) {
+            return TOP_RANK;
+        }
+        if (title.length() > LONG_TITLE_LENGTH) {
+            return baseRank + 1;
+        }
+        return baseRank;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/search/query/DatabaseResultTask.java b/src/com/android/settings/intelligence/search/query/DatabaseResultTask.java
new file mode 100644
index 0000000..65d190a
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/query/DatabaseResultTask.java
@@ -0,0 +1,343 @@
+/*
+ * 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.query;
+
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.Tables
+        .TABLE_PREFS_INDEX;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto;
+import com.android.settings.intelligence.overlay.FeatureFactory;
+import com.android.settings.intelligence.search.SearchFeatureProvider;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper;
+import com.android.settings.intelligence.search.sitemap.SiteMapManager;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * AsyncTask to retrieve Settings, first party app and any intent based results.
+ */
+public class DatabaseResultTask extends SearchQueryTask.QueryWorker {
+
+    private static final String TAG = "DatabaseResultTask";
+
+    public static final String[] SELECT_COLUMNS = {
+            IndexColumns.DATA_TITLE,
+            IndexColumns.DATA_SUMMARY_ON,
+            IndexColumns.DATA_SUMMARY_OFF,
+            IndexColumns.CLASS_NAME,
+            IndexColumns.SCREEN_TITLE,
+            IndexColumns.ICON,
+            IndexColumns.INTENT_ACTION,
+            IndexColumns.DATA_PACKAGE,
+            IndexColumns.INTENT_TARGET_PACKAGE,
+            IndexColumns.INTENT_TARGET_CLASS,
+            IndexColumns.DATA_KEY_REF,
+            IndexColumns.PAYLOAD_TYPE,
+            IndexColumns.PAYLOAD
+    };
+
+    public static final String[] MATCH_COLUMNS_PRIMARY = {
+            IndexColumns.DATA_TITLE,
+            IndexColumns.DATA_TITLE_NORMALIZED,
+    };
+
+    public static final String[] MATCH_COLUMNS_SECONDARY = {
+            IndexColumns.DATA_SUMMARY_ON,
+            IndexColumns.DATA_SUMMARY_ON_NORMALIZED,
+            IndexColumns.DATA_SUMMARY_OFF,
+            IndexColumns.DATA_SUMMARY_OFF_NORMALIZED,
+    };
+
+    public static final int QUERY_WORKER_ID =
+            SettingsIntelligenceLogProto.SettingsIntelligenceEvent.SEARCH_QUERY_DATABASE;
+
+    /**
+     * Base ranks defines the best possible rank based on what the query matches.
+     * If the query matches the prefix of the first word in the title, the best rank it can be
+     * is 1
+     * If the query matches the prefix of the other words in the title, the best rank it can be
+     * is 3
+     * If the query only matches the summary, the best rank it can be is 7
+     * If the query only matches keywords or entries, the best rank it can be is 9
+     */
+    static final int[] BASE_RANKS = {1, 3, 7, 9};
+
+    public static SearchQueryTask newTask(Context context, SiteMapManager siteMapManager,
+            String query) {
+        return new SearchQueryTask(new DatabaseResultTask(context, siteMapManager, query));
+    }
+
+    public final String[] MATCH_COLUMNS_TERTIARY = {
+            IndexColumns.DATA_KEYWORDS,
+            IndexColumns.DATA_ENTRIES
+    };
+
+    private final CursorToSearchResultConverter mConverter;
+    private final SearchFeatureProvider mFeatureProvider;
+
+    public DatabaseResultTask(Context context, SiteMapManager siteMapManager, String queryText) {
+        super(context, siteMapManager, queryText);
+        mConverter = new CursorToSearchResultConverter(context);
+        mFeatureProvider = FeatureFactory.get(context).searchFeatureProvider();
+    }
+
+    @Override
+    protected int getQueryWorkerId() {
+        return QUERY_WORKER_ID;
+    }
+
+    @Override
+    protected List<? extends SearchResult> query() {
+        if (mQuery == null || mQuery.isEmpty()) {
+            return new ArrayList<>();
+        }
+        // Start a Future to get search result scores.
+        FutureTask<List<Pair<String, Float>>> rankerTask = mFeatureProvider.getRankerTask(
+                mContext, mQuery);
+
+        if (rankerTask != null) {
+            ExecutorService executorService = mFeatureProvider.getExecutorService();
+            executorService.execute(rankerTask);
+        }
+
+        final Set<SearchResult> resultSet = new HashSet<>();
+
+        resultSet.addAll(firstWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[0]));
+        resultSet.addAll(secondaryWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[1]));
+        resultSet.addAll(anyWordQuery(MATCH_COLUMNS_SECONDARY, BASE_RANKS[2]));
+        resultSet.addAll(anyWordQuery(MATCH_COLUMNS_TERTIARY, BASE_RANKS[3]));
+
+        // Try to retrieve the scores in time. Otherwise use static ranking.
+        if (rankerTask != null) {
+            try {
+                final long timeoutMs = mFeatureProvider.smartSearchRankingTimeoutMs(mContext);
+                List<Pair<String, Float>> searchRankScores = rankerTask.get(timeoutMs,
+                        TimeUnit.MILLISECONDS);
+                return getDynamicRankedResults(resultSet, searchRankScores);
+            } catch (TimeoutException | InterruptedException | ExecutionException e) {
+                Log.d(TAG, "Error waiting for result scores: " + e);
+            }
+        }
+
+        List<SearchResult> resultList = new ArrayList<>(resultSet);
+        Collections.sort(resultList);
+        return resultList;
+    }
+
+    // TODO (b/33577327) Retrieve all search results with a single query.
+
+    /**
+     * Creates and executes the query which matches prefixes of the first word of the given
+     * columns.
+     *
+     * @param matchColumns The columns to match on
+     * @param baseRank     The highest rank achievable by these results
+     * @return A set of the matching results.
+     */
+    private Set<SearchResult> firstWordQuery(String[] matchColumns, int baseRank) {
+        final String whereClause = buildSingleWordWhereClause(matchColumns);
+        final String query = mQuery + "%";
+        final String[] selection = buildSingleWordSelection(query, matchColumns.length);
+
+        return query(whereClause, selection, baseRank);
+    }
+
+    /**
+     * Creates and executes the query which matches prefixes of the non-first words of the
+     * given columns.
+     *
+     * @param matchColumns The columns to match on
+     * @param baseRank     The highest rank achievable by these results
+     * @return A set of the matching results.
+     */
+    private Set<SearchResult> secondaryWordQuery(String[] matchColumns, int baseRank) {
+        final String whereClause = buildSingleWordWhereClause(matchColumns);
+        final String query = "% " + mQuery + "%";
+        final String[] selection = buildSingleWordSelection(query, matchColumns.length);
+
+        return query(whereClause, selection, baseRank);
+    }
+
+    /**
+     * Creates and executes the query which matches prefixes of the any word of the given
+     * columns.
+     *
+     * @param matchColumns The columns to match on
+     * @param baseRank     The highest rank achievable by these results
+     * @return A set of the matching results.
+     */
+    private Set<SearchResult> anyWordQuery(String[] matchColumns, int baseRank) {
+        final String whereClause = buildTwoWordWhereClause(matchColumns);
+        final String[] selection = buildAnyWordSelection(matchColumns.length * 2);
+
+        return query(whereClause, selection, baseRank);
+    }
+
+    /**
+     * Generic method used by all of the query methods above to execute a query.
+     *
+     * @param whereClause Where clause for the SQL query which uses bindings.
+     * @param selection   List of the transformed query to match each bind in the whereClause
+     * @param baseRank    The highest rank achievable by these results.
+     * @return A set of the matching results.
+     */
+    private Set<SearchResult> query(String whereClause, String[] selection, int baseRank) {
+        final SQLiteDatabase database =
+                IndexDatabaseHelper.getInstance(mContext).getReadableDatabase();
+        try (Cursor resultCursor = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
+                whereClause,
+                selection, null, null, null)) {
+            return mConverter.convertCursor(resultCursor, baseRank, mSiteMapManager);
+        }
+    }
+
+    /**
+     * Builds the SQLite WHERE clause that matches all matchColumns for a single query.
+     *
+     * @param matchColumns List of columns that will be used for matching.
+     * @return The constructed WHERE clause.
+     */
+    private static String buildSingleWordWhereClause(String[] matchColumns) {
+        StringBuilder sb = new StringBuilder(" (");
+        final int count = matchColumns.length;
+        for (int n = 0; n < count; n++) {
+            sb.append(matchColumns[n]);
+            sb.append(" like ? ");
+            if (n < count - 1) {
+                sb.append(" OR ");
+            }
+        }
+        sb.append(") AND enabled = 1");
+        return sb.toString();
+    }
+
+    /**
+     * Builds the SQLite WHERE clause that matches all matchColumns to two different queries.
+     *
+     * @param matchColumns List of columns that will be used for matching.
+     * @return The constructed WHERE clause.
+     */
+    private static String buildTwoWordWhereClause(String[] matchColumns) {
+        StringBuilder sb = new StringBuilder(" (");
+        final int count = matchColumns.length;
+        for (int n = 0; n < count; n++) {
+            sb.append(matchColumns[n]);
+            sb.append(" like ? OR ");
+            sb.append(matchColumns[n]);
+            sb.append(" like ?");
+            if (n < count - 1) {
+                sb.append(" OR ");
+            }
+        }
+        sb.append(") AND enabled = 1");
+        return sb.toString();
+    }
+
+    /**
+     * Fills out the selection array to match the query as the prefix of a single word.
+     *
+     * @param size is the number of columns to be matched.
+     */
+    private String[] buildSingleWordSelection(String query, int size) {
+        String[] selection = new String[size];
+
+        for (int i = 0; i < size; i++) {
+            selection[i] = query;
+        }
+        return selection;
+    }
+
+    /**
+     * Fills out the selection array to match the query as the prefix of a word.
+     *
+     * @param size is twice the number of columns to be matched. The first match is for the
+     *             prefix
+     *             of the first word in the column. The second match is for any subsequent word
+     *             prefix match.
+     */
+    private String[] buildAnyWordSelection(int size) {
+        String[] selection = new String[size];
+        final String query = mQuery + "%";
+        final String subStringQuery = "% " + mQuery + "%";
+
+        for (int i = 0; i < (size - 1); i += 2) {
+            selection[i] = query;
+            selection[i + 1] = subStringQuery;
+        }
+        return selection;
+    }
+
+    private List<SearchResult> getDynamicRankedResults(Set<SearchResult> unsortedSet,
+            final List<Pair<String, Float>> searchRankScores) {
+        final TreeSet<SearchResult> dbResultsSortedByScores = new TreeSet<>(
+                new Comparator<SearchResult>() {
+                    @Override
+                    public int compare(SearchResult o1, SearchResult o2) {
+                        final float score1 = getRankingScoreByKey(searchRankScores, o1.dataKey);
+                        final float score2 = getRankingScoreByKey(searchRankScores, o2.dataKey);
+                        if (score1 > score2) {
+                            return -1;
+                        } else {
+                            return 1;
+                        }
+                    }
+                });
+        dbResultsSortedByScores.addAll(unsortedSet);
+
+        return new ArrayList<>(dbResultsSortedByScores);
+    }
+
+    /**
+     * Looks up ranking score by key.
+     *
+     * @param key key for a single search result.
+     * @return the ranking score corresponding to the given stableId. If there is no score
+     * available for this stableId, -Float.MAX_VALUE is returned.
+     */
+    @VisibleForTesting
+    Float getRankingScoreByKey(List<Pair<String, Float>> searchRankScores, String key) {
+        for (Pair<String, Float> rankingScore : searchRankScores) {
+            if (key.compareTo(rankingScore.first) == 0) {
+                return rankingScore.second;
+            }
+        }
+        // If key not found in the list, we assign the minimum score so it will appear at
+        // the end of the list.
+        Log.w(TAG, key + " was not in the ranking scores.");
+        return -Float.MAX_VALUE;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/search/query/InputDeviceResultTask.java b/src/com/android/settings/intelligence/search/query/InputDeviceResultTask.java
new file mode 100644
index 0000000..e98ea0d
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/query/InputDeviceResultTask.java
@@ -0,0 +1,233 @@
+/*
+ * 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.query;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.icu.text.ListFormatter;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto;
+import com.android.settings.intelligence.search.ResultPayload;
+import com.android.settings.intelligence.search.SearchFeatureProvider;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.indexing.DatabaseIndexingUtils;
+import com.android.settings.intelligence.search.sitemap.SiteMapManager;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+public class InputDeviceResultTask extends SearchQueryTask.QueryWorker {
+
+    private static final String TAG = "InputResultFutureTask";
+
+    public static final int QUERY_WORKER_ID =
+            SettingsIntelligenceLogProto.SettingsIntelligenceEvent.SEARCH_QUERY_INPUT_DEVICES;
+
+    @VisibleForTesting
+    static final String PHYSICAL_KEYBOARD_FRAGMENT =
+            "com.android.settings.inputmethod.PhysicalKeyboardFragment";
+    @VisibleForTesting
+    static final String VIRTUAL_KEYBOARD_FRAGMENT =
+            "com.android.settings.inputmethod.AvailableVirtualKeyboardFragment";
+
+    public static SearchQueryTask newTask(Context context, SiteMapManager manager,
+            String query) {
+        return new SearchQueryTask(new InputDeviceResultTask(context, manager, query));
+    }
+
+
+    private static final int NAME_NO_MATCH = -1;
+
+    private final InputMethodManager mImm;
+    private final PackageManager mPackageManager;
+
+    private List<String> mPhysicalKeyboardBreadcrumb;
+    private List<String> mVirtualKeyboardBreadcrumb;
+
+    public InputDeviceResultTask(Context context, SiteMapManager manager, String query) {
+        super(context, manager, query);
+
+        mImm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+        mPackageManager = context.getPackageManager();
+    }
+
+    @Override
+    protected int getQueryWorkerId() {
+        return QUERY_WORKER_ID;
+    }
+
+    @Override
+    protected List<? extends SearchResult> query() {
+        long startTime = System.currentTimeMillis();
+        final List<SearchResult> results = new ArrayList<>();
+        results.addAll(buildPhysicalKeyboardSearchResults());
+        results.addAll(buildVirtualKeyboardSearchResults());
+        Collections.sort(results);
+        if (SearchFeatureProvider.DEBUG) {
+            Log.d(TAG, "Input search loading took:" + (System.currentTimeMillis() - startTime));
+        }
+        return results;
+    }
+
+    private Set<SearchResult> buildPhysicalKeyboardSearchResults() {
+        final Set<SearchResult> results = new HashSet<>();
+        final String screenTitle = mContext.getString(R.string.physical_keyboard_title);
+
+        for (final InputDevice device : getPhysicalFullKeyboards()) {
+            final String deviceName = device.getName();
+            final int wordDiff = SearchQueryUtils.getWordDifference(deviceName, mQuery);
+            if (wordDiff == NAME_NO_MATCH) {
+                continue;
+            }
+            final Intent intent = DatabaseIndexingUtils.buildSearchTrampolineIntent(mContext,
+                    PHYSICAL_KEYBOARD_FRAGMENT, deviceName, screenTitle);
+            results.add(new SearchResult.Builder()
+                    .setTitle(deviceName)
+                    .setPayload(new ResultPayload(intent))
+                    .setDataKey(deviceName)
+                    .setRank(wordDiff)
+                    .addBreadcrumbs(getPhysicalKeyboardBreadCrumb())
+                    .build());
+        }
+        return results;
+    }
+
+    private Set<SearchResult> buildVirtualKeyboardSearchResults() {
+        final Set<SearchResult> results = new HashSet<>();
+        final String screenTitle = mContext.getString(R.string.add_virtual_keyboard);
+        final List<InputMethodInfo> inputMethods = mImm.getInputMethodList();
+        for (InputMethodInfo info : inputMethods) {
+            final String title = info.loadLabel(mPackageManager).toString();
+            final String summary = getSubtypeLocaleNameListAsSentence(
+                    getAllSubtypesOf(info), mContext, info);
+            int wordDiff = SearchQueryUtils.getWordDifference(title, mQuery);
+            if (wordDiff == NAME_NO_MATCH) {
+                wordDiff = SearchQueryUtils.getWordDifference(summary, mQuery);
+            }
+            if (wordDiff == NAME_NO_MATCH) {
+                continue;
+            }
+            final ServiceInfo serviceInfo = info.getServiceInfo();
+            final String key = new ComponentName(serviceInfo.packageName, serviceInfo.name)
+                    .flattenToString();
+            final Intent intent = DatabaseIndexingUtils.buildSearchTrampolineIntent(mContext,
+                    VIRTUAL_KEYBOARD_FRAGMENT, key, screenTitle);
+            results.add(new SearchResult.Builder()
+                    .setTitle(title)
+                    .setSummary(summary)
+                    .setRank(wordDiff)
+                    .setDataKey(key)
+                    .addBreadcrumbs(getVirtualKeyboardBreadCrumb())
+                    .setPayload(new ResultPayload(intent))
+                    .build());
+        }
+        return results;
+    }
+
+    private List<String> getPhysicalKeyboardBreadCrumb() {
+        if (mPhysicalKeyboardBreadcrumb == null || mPhysicalKeyboardBreadcrumb.isEmpty()) {
+            mPhysicalKeyboardBreadcrumb = mSiteMapManager.buildBreadCrumb(
+                    mContext, PHYSICAL_KEYBOARD_FRAGMENT,
+                    mContext.getString(R.string.physical_keyboard_title));
+        }
+        return mPhysicalKeyboardBreadcrumb;
+    }
+
+
+    private List<String> getVirtualKeyboardBreadCrumb() {
+        if (mVirtualKeyboardBreadcrumb == null || mVirtualKeyboardBreadcrumb.isEmpty()) {
+            final Context context = mContext;
+            mVirtualKeyboardBreadcrumb = mSiteMapManager.buildBreadCrumb(
+                    context, VIRTUAL_KEYBOARD_FRAGMENT,
+                    context.getString(R.string.add_virtual_keyboard));
+        }
+        return mVirtualKeyboardBreadcrumb;
+    }
+
+    private List<InputDevice> getPhysicalFullKeyboards() {
+        final List<InputDevice> keyboards = new ArrayList<>();
+        final int[] deviceIds = InputDevice.getDeviceIds();
+        if (deviceIds != null) {
+            for (int deviceId : deviceIds) {
+                final InputDevice device = InputDevice.getDevice(deviceId);
+                if (isFullPhysicalKeyboard(device)) {
+                    keyboards.add(device);
+                }
+            }
+        }
+        return keyboards;
+    }
+
+    @NonNull
+    private static String getSubtypeLocaleNameListAsSentence(
+            @NonNull final List<InputMethodSubtype> subtypes, @NonNull final Context context,
+            @NonNull final InputMethodInfo inputMethodInfo) {
+        if (subtypes.isEmpty()) {
+            return "";
+        }
+        final Locale locale = Locale.getDefault();
+        final int subtypeCount = subtypes.size();
+        final CharSequence[] subtypeNames = new CharSequence[subtypeCount];
+        for (int i = 0; i < subtypeCount; i++) {
+            subtypeNames[i] = subtypes.get(i).getDisplayName(context,
+                    inputMethodInfo.getPackageName(), inputMethodInfo.getServiceInfo()
+                            .applicationInfo);
+        }
+        return toSentenceCase(
+                ListFormatter.getInstance(locale).format((Object[]) subtypeNames), locale);
+    }
+
+    private static String toSentenceCase(String str, Locale locale) {
+        if (str.isEmpty()) {
+            return str;
+        }
+        final int firstCodePointLen = str.offsetByCodePoints(0, 1);
+        return str.substring(0, firstCodePointLen).toUpperCase(locale)
+                + str.substring(firstCodePointLen);
+    }
+
+    private static boolean isFullPhysicalKeyboard(InputDevice device) {
+        return device != null && !device.isVirtual() &&
+                (device.getSources() & InputDevice.SOURCE_KEYBOARD)
+                        == InputDevice.SOURCE_KEYBOARD
+                && device.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC;
+    }
+
+    private static List<InputMethodSubtype> getAllSubtypesOf(final InputMethodInfo imi) {
+        final int subtypeCount = imi.getSubtypeCount();
+        final List<InputMethodSubtype> allSubtypes = new ArrayList<>(subtypeCount);
+        for (int index = 0; index < subtypeCount; index++) {
+            allSubtypes.add(imi.getSubtypeAt(index));
+        }
+        return allSubtypes;
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/query/InstalledAppResultTask.java b/src/com/android/settings/intelligence/search/query/InstalledAppResultTask.java
new file mode 100644
index 0000000..a14850c
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/query/InstalledAppResultTask.java
@@ -0,0 +1,124 @@
+/*
+ * 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.query;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.provider.Settings;
+
+import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto;
+import com.android.settings.intelligence.search.AppSearchResult;
+import com.android.settings.intelligence.search.ResultPayload;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.indexing.DatabaseIndexingUtils;
+import com.android.settings.intelligence.search.sitemap.SiteMapManager;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Search loader for installed apps.
+ */
+public class InstalledAppResultTask extends SearchQueryTask.QueryWorker {
+
+    public static final int QUERY_WORKER_ID =
+            SettingsIntelligenceLogProto.SettingsIntelligenceEvent.SEARCH_QUERY_INSTALLED_APPS;
+
+    private final PackageManager mPackageManager;
+    private final String INTENT_SCHEME = "package";
+    private List<String> mBreadcrumb;
+
+    public static SearchQueryTask newTask(Context context, SiteMapManager siteMapManager,
+            String query) {
+        return new SearchQueryTask(new InstalledAppResultTask(context, siteMapManager, query));
+    }
+
+    public InstalledAppResultTask(Context context, SiteMapManager siteMapManager,
+            String query) {
+        super(context, siteMapManager, query);
+        mPackageManager = context.getPackageManager();
+    }
+
+    @Override
+    protected int getQueryWorkerId() {
+        return QUERY_WORKER_ID;
+    }
+
+    @Override
+    protected List<? extends SearchResult> query() {
+        final List<AppSearchResult> results = new ArrayList<>();
+
+        List<ApplicationInfo> appsInfo = mPackageManager.getInstalledApplications(
+                PackageManager.MATCH_DISABLED_COMPONENTS
+                        | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS);
+
+        for (ApplicationInfo info : appsInfo) {
+            final CharSequence label = info.loadLabel(mPackageManager);
+            final int wordDiff = SearchQueryUtils.getWordDifference(label.toString(), mQuery);
+            if (wordDiff == SearchQueryUtils.NAME_NO_MATCH) {
+                continue;
+            }
+
+            final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+                    .setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+                    .setData(
+                            Uri.fromParts(INTENT_SCHEME, info.packageName, null /* fragment */))
+                    .putExtra(DatabaseIndexingUtils.EXTRA_SOURCE_METRICS_CATEGORY,
+                            DatabaseIndexingUtils.DASHBOARD_SEARCH_RESULTS);
+
+            final AppSearchResult.Builder builder = new AppSearchResult.Builder();
+            builder.setAppInfo(info)
+                    .setDataKey(info.packageName)
+                    .setTitle(info.loadLabel(mPackageManager))
+                    .setRank(getRank(wordDiff))
+                    .addBreadcrumbs(getBreadCrumb())
+                    .setPayload(new ResultPayload(intent));
+            results.add(builder.build());
+        }
+
+        Collections.sort(results);
+        return results;
+    }
+
+    private List<String> getBreadCrumb() {
+        if (mBreadcrumb == null || mBreadcrumb.isEmpty()) {
+            mBreadcrumb = mSiteMapManager.buildBreadCrumb(
+                    mContext, "com.android.settings.applications.ManageApplications",
+                    mContext.getString(R.string.applications_settings));
+        }
+        return mBreadcrumb;
+    }
+
+    /**
+     * A temporary ranking scheme for installed apps.
+     *
+     * @param wordDiff difference between query length and app name length.
+     * @return the ranking.
+     */
+    private int getRank(int wordDiff) {
+        if (wordDiff < 6) {
+            return 2;
+        }
+        return 3;
+    }
+}
+
diff --git a/src/com/android/settings/intelligence/search/query/SearchQueryTask.java b/src/com/android/settings/intelligence/search/query/SearchQueryTask.java
new file mode 100644
index 0000000..be0546b
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/query/SearchQueryTask.java
@@ -0,0 +1,71 @@
+/*
+ * 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.query;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+import com.android.settings.intelligence.overlay.FeatureFactory;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.sitemap.SiteMapManager;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
+
+public class SearchQueryTask extends FutureTask<List<? extends SearchResult>> {
+
+    private final int mId;
+
+    public SearchQueryTask(@NonNull QueryWorker queryWorker) {
+        super(queryWorker);
+        mId = queryWorker.getQueryWorkerId();
+    }
+
+    public int getTaskId() {
+        return mId;
+    }
+
+    public static abstract class QueryWorker implements Callable<List<? extends SearchResult>> {
+
+        protected final Context mContext;
+        protected final SiteMapManager mSiteMapManager;
+        protected final String mQuery;
+
+        public QueryWorker(Context context, SiteMapManager siteMapManager, String query) {
+            mContext = context;
+            mSiteMapManager = siteMapManager;
+            mQuery = query;
+        }
+
+        @Override
+        public List<? extends SearchResult> call() throws Exception {
+            final long startTime = System.currentTimeMillis();
+            try {
+                return query();
+            } finally {
+                final long endTime = System.currentTimeMillis();
+                FeatureFactory.get(mContext).metricsFeatureProvider(mContext)
+                        .logEvent(getQueryWorkerId(), endTime - startTime);
+            }
+        }
+
+        protected abstract int getQueryWorkerId();
+
+        protected abstract List<? extends SearchResult> query();
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/query/SearchQueryUtils.java b/src/com/android/settings/intelligence/search/query/SearchQueryUtils.java
new file mode 100644
index 0000000..d7011ee
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/query/SearchQueryUtils.java
@@ -0,0 +1,91 @@
+/*
+ * 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.query;
+
+import android.text.TextUtils;
+
+/**
+ * Utils for Query-time operations.
+ */
+
+public class SearchQueryUtils {
+
+    public static final int NAME_NO_MATCH = -1;
+
+    /**
+     * Returns "difference" between resultName and query string. resultName must contain all
+     * characters from query as a prefix to a word, in the same order.
+     * If not, returns NAME_NO_MATCH.
+     * If they do match, returns an int value representing  how different they are,
+     * and larger values means they are less similar.
+     * <p/>
+     * Example:
+     * resultName: Abcde, query: Abcde, Returns 0
+     * resultName: Abcde, query: abc, Returns 2
+     * resultName: Abcde, query: ab, Returns 3
+     * resultName: Abcde, query: bc, Returns NAME_NO_MATCH
+     * resultName: Abcde, query: xyz, Returns NAME_NO_MATCH
+     * resultName: Abc de, query: de, Returns 4
+     */
+    public static int getWordDifference(String resultName, String query) {
+        if (TextUtils.isEmpty(resultName) || TextUtils.isEmpty(query)) {
+            return NAME_NO_MATCH;
+        }
+
+        final char[] queryTokens = query.toLowerCase().toCharArray();
+        final char[] resultTokens = resultName.toLowerCase().toCharArray();
+        final int resultLength = resultTokens.length;
+        if (queryTokens.length > resultLength) {
+            return NAME_NO_MATCH;
+        }
+
+        int i = 0;
+        int j;
+
+        while (i < resultLength) {
+            j = 0;
+            // Currently matching a prefix
+            while ((i + j < resultLength) && (queryTokens[j] == resultTokens[i + j])) {
+                // Matched the entire query
+                if (++j >= queryTokens.length) {
+                    // Use the diff in length as a proxy of how close the 2 words match.
+                    // Value range from 0 to infinity.
+                    return resultLength - queryTokens.length;
+                }
+            }
+
+            i += j;
+
+            // Remaining string is longer that the query or we have search the whole result name.
+            if (queryTokens.length > resultLength - i) {
+                return NAME_NO_MATCH;
+            }
+
+            // This is the first index where result name and query name are different
+            // Find the next space in the result name or the end of the result name.
+            while ((i < resultLength) && (!Character.isWhitespace(resultTokens[i++]))) ;
+
+            // Find the start of the next word
+            while ((i < resultLength) && !(Character.isLetter(resultTokens[i])
+                    || Character.isDigit(resultTokens[i]))) {
+                // Increment in body because we cannot guarantee which condition was true
+                i++;
+            }
+        }
+        return NAME_NO_MATCH;
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/ranking/SearchResultsRankerCallback.java b/src/com/android/settings/intelligence/search/ranking/SearchResultsRankerCallback.java
new file mode 100644
index 0000000..caddffc
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/ranking/SearchResultsRankerCallback.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.intelligence.search.ranking;
+
+import android.util.Pair;
+
+import java.util.List;
+
+public interface SearchResultsRankerCallback {
+
+    /**
+     * Called when ranker provides the ranking scores.
+     * @param searchRankingScores Ordered List of Pairs of String and Float corresponding to
+     *                            stableIds and ranking scores. The list must be descendingly
+     *                            ordered based on scores.
+     */
+    public void onRankingScoresAvailable(List<Pair<String, Float>> searchRankingScores);
+
+    /**
+     * Called when for any reason ranker fails, which notifies the client to proceed
+     * without ranking results.
+     */
+    public void onRankingFailed();
+}
diff --git a/src/com/android/settings/intelligence/search/savedqueries/SavedQueryController.java b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryController.java
new file mode 100644
index 0000000..e4c2cc1
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryController.java
@@ -0,0 +1,130 @@
+/*
+ * 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.savedqueries;
+
+import android.app.LoaderManager;
+import android.content.Context;
+import android.content.Loader;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.overlay.FeatureFactory;
+import com.android.settings.intelligence.search.SearchFeatureProvider;
+import com.android.settings.intelligence.search.SearchFragment;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.SearchResultsAdapter;
+
+import java.util.List;
+
+public class SavedQueryController implements LoaderManager.LoaderCallbacks,
+        MenuItem.OnMenuItemClickListener {
+
+    // TODO: make a generic background task manager to handle one-off tasks like this one.
+    private static final String ARG_QUERY = "remove_query";
+    private static final String TAG = "SearchSavedQueryCtrl";
+
+    private static final int MENU_SEARCH_HISTORY = 1000;
+
+    private final Context mContext;
+    private final LoaderManager mLoaderManager;
+    private final SearchFeatureProvider mSearchFeatureProvider;
+    private final SearchResultsAdapter mResultAdapter;
+
+    public SavedQueryController(Context context, LoaderManager loaderManager,
+            SearchResultsAdapter resultsAdapter) {
+        mContext = context;
+        mLoaderManager = loaderManager;
+        mResultAdapter = resultsAdapter;
+        mSearchFeatureProvider = FeatureFactory.get(context)
+                .searchFeatureProvider();
+    }
+
+    @Override
+    public Loader onCreateLoader(int id, Bundle args) {
+        switch (id) {
+            case SearchFragment.SearchLoaderId.SAVE_QUERY_TASK:
+                return new SavedQueryRecorder(mContext, args.getString(ARG_QUERY));
+            case SearchFragment.SearchLoaderId.REMOVE_QUERY_TASK:
+                return new SavedQueryRemover(mContext);
+            case SearchFragment.SearchLoaderId.SAVED_QUERIES:
+                return mSearchFeatureProvider.getSavedQueryLoader(mContext);
+        }
+        return null;
+    }
+
+    @Override
+    public void onLoadFinished(Loader loader, Object data) {
+        switch (loader.getId()) {
+            case SearchFragment.SearchLoaderId.REMOVE_QUERY_TASK:
+                mLoaderManager.restartLoader(SearchFragment.SearchLoaderId.SAVED_QUERIES,
+                        null /* args */, this /* callback */);
+                break;
+            case SearchFragment.SearchLoaderId.SAVED_QUERIES:
+                if (SearchFeatureProvider.DEBUG) {
+                    Log.d(TAG, "Saved queries loaded");
+                }
+                mResultAdapter.displaySavedQuery((List<SearchResult>) data);
+                break;
+        }
+    }
+
+    @Override
+    public void onLoaderReset(Loader loader) {
+    }
+
+    @Override
+    public boolean onMenuItemClick(MenuItem item) {
+        if (item.getItemId() != MENU_SEARCH_HISTORY) {
+            return false;
+        }
+        removeQueries();
+        return true;
+    }
+
+    public void buildMenuItem(Menu menu) {
+        final MenuItem item =
+                menu.add(Menu.NONE, MENU_SEARCH_HISTORY, Menu.NONE, R.string.search_clear_history);
+        item.setOnMenuItemClickListener(this);
+    }
+
+    public void saveQuery(String query) {
+        final Bundle args = new Bundle();
+        args.putString(ARG_QUERY, query);
+        mLoaderManager.restartLoader(SearchFragment.SearchLoaderId.SAVE_QUERY_TASK, args,
+                this /* callback */);
+    }
+
+    /**
+     * Remove all saved queries from DB
+     */
+    public void removeQueries() {
+        final Bundle args = new Bundle();
+        mLoaderManager.restartLoader(SearchFragment.SearchLoaderId.REMOVE_QUERY_TASK, args,
+                this /* callback */);
+    }
+
+    public void loadSavedQueries() {
+        if (SearchFeatureProvider.DEBUG) {
+            Log.d(TAG, "loading saved queries");
+        }
+        mLoaderManager.restartLoader(SearchFragment.SearchLoaderId.SAVED_QUERIES, null /* args */,
+                this /* callback */);
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/savedqueries/SavedQueryLoader.java b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryLoader.java
new file mode 100644
index 0000000..9d0316c
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryLoader.java
@@ -0,0 +1,81 @@
+/*
+ * 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.savedqueries;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.support.annotation.VisibleForTesting;
+
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper;
+import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.SavedQueriesColumns;
+import com.android.settings.intelligence.utils.AsyncLoader;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Loader for recently searched queries.
+ */
+public class SavedQueryLoader extends AsyncLoader<List<? extends SearchResult>> {
+
+    // Max number of proposed suggestions
+    @VisibleForTesting
+    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<? extends SearchResult> result) {
+
+    }
+
+    @Override
+    public List<? extends SearchResult> loadInBackground() {
+        try (final 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()
+                    .setDataKey(payload.query)
+                    .setTitle(payload.query)
+                    .setPayload(payload)
+                    .build());
+        }
+        return results;
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/savedqueries/SavedQueryPayload.java b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryPayload.java
new file mode 100644
index 0000000..264eee9
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryPayload.java
@@ -0,0 +1,68 @@
+/*
+ * 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.savedqueries;
+
+import android.os.Parcel;
+import android.support.annotation.VisibleForTesting;
+
+import com.android.settings.intelligence.search.ResultPayload;
+
+/**
+ * {@link ResultPayload} for saved query.
+ */
+public class SavedQueryPayload extends ResultPayload {
+
+    public final String query;
+
+    public SavedQueryPayload(String query) {
+        super(null /* Intent */);
+        this.query = query;
+    }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    SavedQueryPayload(Parcel in) {
+        super(null /* Intent */);
+        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/intelligence/search/savedqueries/SavedQueryRecorder.java b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryRecorder.java
new file mode 100644
index 0000000..618e0e7
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/savedqueries/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.intelligence.search.savedqueries;
+
+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.intelligence.search.indexing.IndexDatabaseHelper;
+import com.android.settings.intelligence.utils.AsyncLoader;
+
+import static com.android.settings.intelligence.search.indexing.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/intelligence/search/savedqueries/SavedQueryRemover.java b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryRemover.java
new file mode 100644
index 0000000..7c600d4
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryRemover.java
@@ -0,0 +1,65 @@
+/*
+ * 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.savedqueries;
+
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.Tables.TABLE_SAVED_QUERIES;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.util.Log;
+
+import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper;
+import com.android.settings.intelligence.utils.AsyncLoader;
+
+
+public class SavedQueryRemover extends AsyncLoader<Void> {
+
+    private static final String LOG_TAG = "SavedQueryRemover";
+
+    public SavedQueryRemover(Context context) {
+        super(context);
+    }
+
+    @Override
+    public Void loadInBackground() {
+        final SQLiteDatabase database = getWritableDatabase();
+        try {
+            // First, delete all saved queries that are the same
+            database.delete(TABLE_SAVED_QUERIES,
+                    null /* where */,
+                    null /* whereArgs */);
+        } catch (Exception e) {
+            Log.d(LOG_TAG, "Cannot update saved Search queries", e);
+        }
+        return null;
+    }
+
+    @Override
+    protected void onDiscardResult(Void result) {
+
+    }
+
+    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/intelligence/search/savedqueries/SavedQueryViewHolder.java b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryViewHolder.java
new file mode 100644
index 0000000..40d94f5
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryViewHolder.java
@@ -0,0 +1,52 @@
+/*
+ * 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.savedqueries;
+
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto;
+import com.android.settings.intelligence.search.SearchFragment;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.SearchViewHolder;
+
+public class SavedQueryViewHolder extends SearchViewHolder {
+
+    public final TextView titleView;
+
+    public SavedQueryViewHolder(View view) {
+        super(view);
+        titleView = view.findViewById(android.R.id.title);
+    }
+
+    @Override
+    public int getClickActionMetricName() {
+        return SettingsIntelligenceLogProto.SettingsIntelligenceEvent.CLICK_SAVED_QUERY;
+    }
+
+    @Override
+    public void onBind(final SearchFragment fragment, final SearchResult result) {
+        itemView.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                fragment.onSavedQueryClicked(SavedQueryViewHolder.this,
+                        result.title);
+            }
+        });
+        titleView.setText(result.title);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/search/sitemap/SiteMapManager.java b/src/com/android/settings/intelligence/search/sitemap/SiteMapManager.java
new file mode 100644
index 0000000..315cd4f
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/sitemap/SiteMapManager.java
@@ -0,0 +1,126 @@
+/*
+ * 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.sitemap;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.support.annotation.WorkerThread;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper;
+import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.SiteMapColumns;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SiteMapManager {
+
+    private static final String TAG = "SiteMapManager";
+    private static final boolean DEBUG_TIMING = false;
+
+    public static final String[] SITE_MAP_COLUMNS = {
+            SiteMapColumns.PARENT_CLASS,
+            SiteMapColumns.PARENT_TITLE,
+            SiteMapColumns.CHILD_CLASS,
+            SiteMapColumns.CHILD_TITLE
+    };
+
+    private final List<SiteMapPair> mPairs = new ArrayList<>();
+
+    private boolean mInitialized;
+
+    /**
+     * Given a fragment class name and its screen title, build a breadcrumb from Settings root to
+     * this screen.
+     * <p/>
+     * Not all screens have a full breadcrumb path leading up to root, it's because either some
+     * page in the breadcrumb path is not indexed, or it's only reachable via search.
+     */
+    @WorkerThread
+    public synchronized List<String> buildBreadCrumb(Context context, String clazz,
+            String screenTitle) {
+        init(context);
+        final long startTime = System.currentTimeMillis();
+        final List<String> breadcrumbs = new ArrayList<>();
+        if (!mInitialized) {
+            Log.w(TAG, "SiteMap is not initialized yet, skipping");
+            return breadcrumbs;
+        }
+        breadcrumbs.add(screenTitle);
+        String currentClass = clazz;
+        String currentTitle = screenTitle;
+        // Look up current page's parent, if found add it to breadcrumb string list, and repeat.
+        while (true) {
+            final SiteMapPair pair = lookUpParent(currentClass, currentTitle);
+            if (pair == null) {
+                if (DEBUG_TIMING) {
+                    Log.d(TAG, "BreadCrumb timing: " + (System.currentTimeMillis() - startTime));
+                }
+                return breadcrumbs;
+            }
+            breadcrumbs.add(0, pair.getParentTitle());
+            currentClass = pair.getParentClass();
+            currentTitle = pair.getParentTitle();
+        }
+    }
+
+    /**
+     * Initialize a list of {@link SiteMapPair}s. Each pair knows about a single parent-child
+     * page relationship.
+     */
+    @WorkerThread
+    private synchronized void init(Context context) {
+        if (mInitialized) {
+            // Make sure only init once.
+            return;
+        }
+        final long startTime = System.currentTimeMillis();
+        // First load site map from static index table.
+        final Context appContext = context.getApplicationContext();
+        final SQLiteDatabase db = IndexDatabaseHelper.getInstance(appContext).getReadableDatabase();
+        Cursor sitemap = db.query(IndexDatabaseHelper.Tables.TABLE_SITE_MAP, SITE_MAP_COLUMNS, null,
+                null, null, null, null);
+        while (sitemap.moveToNext()) {
+            final SiteMapPair pair = new SiteMapPair(
+                    sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.PARENT_CLASS)),
+                    sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.PARENT_TITLE)),
+                    sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.CHILD_CLASS)),
+                    sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.CHILD_TITLE)));
+            mPairs.add(pair);
+        }
+        sitemap.close();
+        // Done.
+        mInitialized = true;
+        if (DEBUG_TIMING) {
+            Log.d(TAG, "Init timing: " + (System.currentTimeMillis() - startTime));
+        }
+    }
+
+    @WorkerThread
+    private SiteMapPair lookUpParent(String clazz, String title) {
+        for (SiteMapPair pair : mPairs) {
+            if (TextUtils.equals(pair.getChildClass(), clazz)
+                    && TextUtils.equals(title, pair.getChildTitle())) {
+                return pair;
+            }
+        }
+        return null;
+    }
+
+}
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/search/sitemap/SiteMapPair.java b/src/com/android/settings/intelligence/search/sitemap/SiteMapPair.java
new file mode 100644
index 0000000..cf30dc4
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/sitemap/SiteMapPair.java
@@ -0,0 +1,88 @@
+/*
+ * 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.sitemap;
+
+import android.content.ContentValues;
+import android.text.TextUtils;
+
+import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper;
+
+import java.util.Objects;
+
+/**
+ * Data model for a parent-child page pair.
+ * <p/>
+ * A list of {@link SiteMapPair} can represent the breadcrumb for a search result from settings.
+ */
+public class SiteMapPair {
+    private final String mParentClass;
+    private final String mParentTitle;
+    private final String mChildClass;
+    private final String mChildTitle;
+
+    public SiteMapPair(String parentClass, String parentTitle, String childClass,
+            String childTitle) {
+        mParentClass = parentClass;
+        mParentTitle = parentTitle;
+        mChildClass = childClass;
+        mChildTitle = childTitle;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mParentClass, mChildClass);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == null || !(other instanceof SiteMapPair)) {
+            return false;
+        }
+        return TextUtils.equals(mParentClass, ((SiteMapPair) other).mParentClass)
+                && TextUtils.equals(mChildClass, ((SiteMapPair) other).mChildClass);
+    }
+
+    public String getParentClass() {
+        return mParentClass;
+    }
+
+    public String getParentTitle() {
+        return mParentTitle;
+    }
+
+    public String getChildClass() {
+        return mChildClass;
+    }
+
+    public String getChildTitle() {
+        return mChildTitle;
+    }
+
+    /**
+     * Converts this object into {@link ContentValues}. The content follows schema in
+     * {@link IndexDatabaseHelper.SiteMapColumns}.
+     */
+    public ContentValues toContentValue() {
+        final ContentValues values = new ContentValues();
+        values.put(IndexDatabaseHelper.SiteMapColumns.DOCID, hashCode());
+        values.put(IndexDatabaseHelper.SiteMapColumns.PARENT_CLASS, mParentClass);
+        values.put(IndexDatabaseHelper.SiteMapColumns.PARENT_TITLE, mParentTitle);
+        values.put(IndexDatabaseHelper.SiteMapColumns.CHILD_CLASS, mChildClass);
+        values.put(IndexDatabaseHelper.SiteMapColumns.CHILD_TITLE, mChildTitle);
+        return values;
+    }
+}
diff --git a/src/com/android/settings/intelligence/suggestions/SuggestionDismissHandler.java b/src/com/android/settings/intelligence/suggestions/SuggestionDismissHandler.java
deleted file mode 100644
index 400227b..0000000
--- a/src/com/android/settings/intelligence/suggestions/SuggestionDismissHandler.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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.suggestions;
-
-import android.content.Context;
-
-public class SuggestionDismissHandler {
-
-    private static final String IS_DISMISSED = "_is_dismissed";
-
-    private static SuggestionDismissHandler sDismissHandler;
-
-    private SuggestionDismissHandler() {}
-
-    public static SuggestionDismissHandler getInstance() {
-        if (sDismissHandler == null) {
-            sDismissHandler = new SuggestionDismissHandler();
-        }
-        return sDismissHandler;
-    }
-
-    public void markSuggestionDismissed(Context context, String id) {
-        SuggestionService.getSharedPrefs(context)
-                .edit()
-                .putBoolean(getDismissKey(id), true)
-                .apply();
-    }
-
-    public void markSuggestionNotDismissed(Context context, String id) {
-        SuggestionService.getSharedPrefs(context)
-                .edit()
-                .putBoolean(getDismissKey(id), false)
-                .apply();
-    }
-
-    public boolean isSuggestionDismissed(Context context, String id) {
-        return SuggestionService.getSharedPrefs(context)
-                .getBoolean(getDismissKey(id), false);
-    }
-
-    private static String getDismissKey(String id) {
-        return id + IS_DISMISSED;
-    }
-}
diff --git a/src/com/android/settings/intelligence/suggestions/SuggestionFeatureProvider.java b/src/com/android/settings/intelligence/suggestions/SuggestionFeatureProvider.java
new file mode 100644
index 0000000..ec89385
--- /dev/null
+++ b/src/com/android/settings/intelligence/suggestions/SuggestionFeatureProvider.java
@@ -0,0 +1,108 @@
+/*
+ * 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.suggestions;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.service.settings.suggestions.Suggestion;
+
+import com.android.settings.intelligence.suggestions.ranking.SuggestionEventStore;
+import com.android.settings.intelligence.suggestions.ranking.SuggestionFeaturizer;
+import com.android.settings.intelligence.suggestions.ranking.SuggestionRanker;
+
+import java.util.List;
+
+public class SuggestionFeatureProvider {
+
+    private static final String SHARED_PREF_FILENAME = "suggestions";
+    private static final String IS_DISMISSED = "_is_dismissed";
+
+    private SuggestionRanker mRanker;
+
+    /**
+     * Returns the {@link SharedPreferences} to store suggestion related user data.
+     */
+    public SharedPreferences getSharedPrefs(Context context) {
+        return context.getApplicationContext()
+                .getSharedPreferences(SHARED_PREF_FILENAME, Context.MODE_PRIVATE);
+    }
+
+    /**
+     * Returns a list of suggestions that UI should display, sorted based on importance.
+     */
+    public List<Suggestion> getSuggestions(Context context) {
+        final SuggestionParser parser = new SuggestionParser(context);
+        final List<Suggestion> list = parser.getSuggestions();
+
+        final List<Suggestion> rankedSuggestions = getRanker(context).rankRelevantSuggestions(list);
+
+        final SuggestionEventStore eventStore = SuggestionEventStore.get(context);
+        for (Suggestion suggestion : rankedSuggestions) {
+            eventStore.writeEvent(suggestion.getId(), SuggestionEventStore.EVENT_SHOWN);
+        }
+        return rankedSuggestions;
+    }
+
+    /**
+     * Mark a suggestion as dismissed
+     */
+    public void markSuggestionDismissed(Context context, String id) {
+        getSharedPrefs(context)
+                .edit()
+                .putBoolean(getDismissKey(id), true)
+                .apply();
+
+        SuggestionEventStore.get(context).writeEvent(id, SuggestionEventStore.EVENT_DISMISSED);
+    }
+
+    /**
+     * Mark a suggestion as not dismissed
+     */
+    public void markSuggestionNotDismissed(Context context, String id) {
+        getSharedPrefs(context)
+                .edit()
+                .putBoolean(getDismissKey(id), false)
+                .apply();
+    }
+
+    public void markSuggestionLaunched(Context context, String id) {
+        SuggestionEventStore.get(context).writeEvent(id, SuggestionEventStore.EVENT_CLICKED);
+    }
+
+    /**
+     * Whether or not a suggestion is dismissed
+     */
+    public boolean isSuggestionDismissed(Context context, String id) {
+        return getSharedPrefs(context)
+                .getBoolean(getDismissKey(id), false);
+    }
+
+    /**
+     * Returns a manager that knows how to rank suggestions.
+     */
+    protected SuggestionRanker getRanker(Context context) {
+        if (mRanker == null) {
+            mRanker = new SuggestionRanker(context,
+                    new SuggestionFeaturizer(context.getApplicationContext()));
+        }
+        return mRanker;
+    }
+
+    private static String getDismissKey(String id) {
+        return id + IS_DISMISSED;
+    }
+}
diff --git a/src/com/android/settings/intelligence/suggestions/SuggestionParser.java b/src/com/android/settings/intelligence/suggestions/SuggestionParser.java
index c5af82e..f7cecd2 100644
--- a/src/com/android/settings/intelligence/suggestions/SuggestionParser.java
+++ b/src/com/android/settings/intelligence/suggestions/SuggestionParser.java
@@ -30,6 +30,8 @@
 import android.util.ArrayMap;
 import android.util.Log;
 
+import com.android.settings.intelligence.overlay.FeatureFactory;
+import com.android.settings.intelligence.suggestions.eligibility.CandidateSuggestionFilter;
 import com.android.settings.intelligence.suggestions.model.CandidateSuggestion;
 import com.android.settings.intelligence.suggestions.model.SuggestionCategory;
 import com.android.settings.intelligence.suggestions.model.SuggestionListBuilder;
@@ -58,7 +60,8 @@
         mContext = context.getApplicationContext();
         mPackageManager = context.getPackageManager();
         mAddCache = new ArrayMap<>();
-        mSharedPrefs = SuggestionService.getSharedPrefs(mContext);
+        mSharedPrefs = FeatureFactory.get(mContext)
+                .suggestionFeatureProvider().getSharedPrefs(mContext);
     }
 
     public List<Suggestion> getSuggestions() {
@@ -96,13 +99,24 @@
         probe.addCategory(category.getCategory());
         List<ResolveInfo> results = mPackageManager
                 .queryIntentActivities(probe, PackageManager.GET_META_DATA);
+
+        // Build a list of eligible candidates
+        final List<CandidateSuggestion> eligibleCandidates = new ArrayList<>();
         for (ResolveInfo resolved : results) {
             final CandidateSuggestion candidate = new CandidateSuggestion(mContext, resolved,
                     ignoreDismissRule);
             if (!candidate.isEligible()) {
                 continue;
             }
+            eligibleCandidates.add(candidate);
+        }
+        // Then remove completed ones
+        final List<CandidateSuggestion> incompleteSuggestions = CandidateSuggestionFilter
+                .getInstance()
+                .filterCandidates(mContext, eligibleCandidates);
 
+        // Convert the rest to suggestion.
+        for (CandidateSuggestion candidate : incompleteSuggestions) {
             final String id = candidate.getId();
             Suggestion suggestion = mAddCache.get(id);
             if (suggestion == null) {
@@ -137,67 +151,4 @@
                 + category.getCategory());
         return elapsedTime > category.getExclusiveExpireDaysInMillis();
     }
-
-//
-//    /**
-//     * Gets text associated with the input key from the content provider.
-//     * @param context context
-//     * @param uriString URI for the content provider
-//     * @param providerMap Maps URI authorities to providers
-//     * @param key Key mapping to the text in bundle returned by the content provider
-//     * @return Text associated with the key, if returned by the content provider
-//     */
-//    public static String getTextFromUri(Context context, String uriString,
-//            Map<String, IContentProvider> providerMap, String key) {
-//        Bundle bundle = getBundleFromUri(context, uriString, providerMap);
-//        return (bundle != null) ? bundle.getString(key) : null;
-//    }
-//
-//    private static Bundle getBundleFromUri(Context context, String uriString,
-//            Map<String, IContentProvider> providerMap) {
-//        if (TextUtils.isEmpty(uriString)) {
-//            return null;
-//        }
-//        Uri uri = Uri.parse(uriString);
-//        String method = getMethodFromUri(uri);
-//        if (TextUtils.isEmpty(method)) {
-//            return null;
-//        }
-//        IContentProvider provider = getProviderFromUri(context, uri, providerMap);
-//        if (provider == null) {
-//            return null;
-//        }
-//        try {
-//            return provider.call(context.getPackageName(), method, uriString, null);
-//        } catch (RemoteException e) {
-//            return null;
-//        }
-//    }
-//
-//    private static IContentProvider getProviderFromUri(Context context, Uri uri,
-//            Map<String, IContentProvider> providerMap) {
-//        if (uri == null) {
-//            return null;
-//        }
-//        String authority = uri.getAuthority();
-//        if (TextUtils.isEmpty(authority)) {
-//            return null;
-//        }
-//        if (!providerMap.containsKey(authority)) {
-//            providerMap.put(authority, context.getContentResolver().acquireUnstableProvider(uri));
-//        }
-//        return providerMap.get(authority);
-//    }
-//
-//    /** Returns the first path segment of the uri if it exists as the method, otherwise null. */
-//    static String getMethodFromUri(Uri uri) {
-//        if (uri == null) {
-//            return null;
-//        }
-//        List<String> pathSegments = uri.getPathSegments();
-//        if ((pathSegments == null) || pathSegments.isEmpty()) {
-//            return null;
-//        }
-//        return pathSegments.get(0);
-//    }
 }
diff --git a/src/com/android/settings/intelligence/suggestions/SuggestionService.java b/src/com/android/settings/intelligence/suggestions/SuggestionService.java
index 630a219..2a90afb 100644
--- a/src/com/android/settings/intelligence/suggestions/SuggestionService.java
+++ b/src/com/android/settings/intelligence/suggestions/SuggestionService.java
@@ -16,44 +16,61 @@
 
 package com.android.settings.intelligence.suggestions;
 
-import android.content.Context;
-import android.content.SharedPreferences;
 import android.service.settings.suggestions.Suggestion;
 import android.util.Log;
 
-import com.android.settings.intelligence.suggestions.ranking.SuggestionRanker;
+import com.android.settings.intelligence.overlay.FeatureFactory;
 
+import java.util.ArrayList;
 import java.util.List;
 
 public class SuggestionService extends android.service.settings.suggestions.SuggestionService {
 
     private static final String TAG = "SuggestionService";
 
-    private static final String SHARED_PREF_FILENAME = "suggestions";
-
     @Override
     public List<Suggestion> onGetSuggestions() {
-        final SuggestionParser parser = new SuggestionParser(this);
-        final List<Suggestion> list = parser.getSuggestions();
-        SuggestionRanker.getInstance(this).rankSuggestions(list);
+        final long startTime = System.currentTimeMillis();
+        final List<Suggestion> list = FeatureFactory.get(this)
+                .suggestionFeatureProvider()
+                .getSuggestions(this);
+
+        final List<String> ids = new ArrayList<>(list.size());
+        for (Suggestion suggestion : list) {
+            ids.add(suggestion.getId());
+        }
+        final long endTime = System.currentTimeMillis();
+        FeatureFactory.get(this)
+                .metricsFeatureProvider(this)
+                .logGetSuggestion(ids, endTime - startTime);
         return list;
     }
 
     @Override
     public void onSuggestionDismissed(Suggestion suggestion) {
+        final long startTime = System.currentTimeMillis();
         final String id = suggestion.getId();
         Log.d(TAG, "dismissing suggestion " + id);
-        SuggestionDismissHandler.getInstance()
+        final long endTime = System.currentTimeMillis();
+        FeatureFactory.get(this)
+                .suggestionFeatureProvider()
                 .markSuggestionDismissed(this /* context */, id);
+        FeatureFactory.get(this)
+                .metricsFeatureProvider(this)
+                .logDismissSuggestion(id, endTime - startTime);
     }
 
     @Override
     public void onSuggestionLaunched(Suggestion suggestion) {
-        Log.d(TAG, "Suggestion launched" + suggestion.getId());
-    }
-
-    public static SharedPreferences getSharedPrefs(Context context) {
-        return context.getApplicationContext()
-                .getSharedPreferences(SHARED_PREF_FILENAME, Context.MODE_PRIVATE);
+        final long startTime = System.currentTimeMillis();
+        final String id = suggestion.getId();
+        Log.d(TAG, "Suggestion is launched" + id);
+        final long endTime = System.currentTimeMillis();
+        FeatureFactory.get(this)
+                .suggestionFeatureProvider()
+                .markSuggestionLaunched(this, id);
+        FeatureFactory.get(this)
+                .metricsFeatureProvider(this)
+                .logLaunchSuggestion(id, endTime - startTime);
     }
 }
diff --git a/src/com/android/settings/intelligence/suggestions/eligibility/CandidateSuggestionFilter.java b/src/com/android/settings/intelligence/suggestions/eligibility/CandidateSuggestionFilter.java
new file mode 100644
index 0000000..5f614f6
--- /dev/null
+++ b/src/com/android/settings/intelligence/suggestions/eligibility/CandidateSuggestionFilter.java
@@ -0,0 +1,172 @@
+/*
+ * 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.suggestions.eligibility;
+
+import static android.content.Intent.EXTRA_COMPONENT_NAME;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ProviderInfo;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.settings.intelligence.suggestions.model.CandidateSuggestion;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Filters candidate list to only valid ones.
+ */
+public class CandidateSuggestionFilter {
+
+    private static final String TAG = "CandidateSuggestionFilter";
+    private static final long CHECK_TASK_TIMEOUT_MS = 200;
+
+    private static CandidateSuggestionFilter sChecker;
+    private static ExecutorService sExecutorService;
+
+    public static CandidateSuggestionFilter getInstance() {
+        if (sChecker == null) {
+            sChecker = new CandidateSuggestionFilter();
+            sExecutorService = Executors.newCachedThreadPool();
+        }
+        return sChecker;
+    }
+
+    @NonNull
+    public synchronized List<CandidateSuggestion> filterCandidates(Context context,
+            List<CandidateSuggestion> candidates) {
+        final long startTime = System.currentTimeMillis();
+        final List<CandidateFilterTask> checkTasks = new ArrayList<>();
+        final List<CandidateSuggestion> incompleteCandidates = new ArrayList<>();
+        if (candidates == null) {
+            return incompleteCandidates;
+        }
+        // Put a check task into ExecutorService for each candidate.
+        for (CandidateSuggestion candidate : candidates) {
+            final CandidateFilterTask task = new CandidateFilterTask(context, candidate);
+            sExecutorService.execute(task);
+            checkTasks.add(task);
+        }
+        for (CandidateFilterTask task : checkTasks) {
+            try {
+                final CandidateSuggestion candidate = task.get(CHECK_TASK_TIMEOUT_MS,
+                        TimeUnit.MILLISECONDS);
+                if (candidate != null) {
+                    incompleteCandidates.add(candidate);
+                }
+            } catch (TimeoutException | InterruptedException | ExecutionException e) {
+                Log.w(TAG, "Error checking completion state for " + task.getId());
+            }
+        }
+        final long endTime = System.currentTimeMillis();
+        Log.d(TAG, "filterCandidates duration: " + (endTime - startTime));
+        return incompleteCandidates;
+    }
+
+    /**
+     * {@link FutureTask} that filters status for a suggestion candidate.
+     * <p/>
+     * If the candidate status is valid, {@link #get()} will return the candidate itself.
+     * Otherwise it returns null.
+     */
+    static class CandidateFilterTask extends FutureTask<CandidateSuggestion> {
+
+        private static final String EXTRA_CANDIDATE_ID = "candidate_id";
+        private static final String RESULT_IS_COMPLETE = "candidate_is_complete";
+
+        private final String mId;
+
+        public CandidateFilterTask(Context context, CandidateSuggestion candidate) {
+            super(new GetSuggestionStatusCallable(context, candidate));
+            mId = candidate.getId();
+        }
+
+        public String getId() {
+            return mId;
+        }
+
+        @VisibleForTesting
+        static class GetSuggestionStatusCallable implements Callable<CandidateSuggestion> {
+            @VisibleForTesting
+            static final String CONTENT_PROVIDER_INTENT_ACTION =
+                    "com.android.settings.action.SUGGESTION_STATE_PROVIDER";
+            private static final String METHOD_GET_SUGGESTION_STATE = "getSuggestionState";
+
+            private final Context mContext;
+            private final CandidateSuggestion mCandidate;
+
+            public GetSuggestionStatusCallable(Context context, CandidateSuggestion candidate) {
+                mContext = context.getApplicationContext();
+                mCandidate = candidate;
+            }
+
+            @Override
+            public CandidateSuggestion call() throws Exception {
+                // First find if candidate has any state provider.
+                final String packageName = mCandidate.getComponent().getPackageName();
+                final Intent probe = new Intent(CONTENT_PROVIDER_INTENT_ACTION)
+                        .setPackage(packageName);
+                final List<ResolveInfo> providers = mContext.getPackageManager()
+                        .queryIntentContentProviders(probe, 0 /* flags */);
+                if (providers == null || providers.isEmpty()) {
+                    // No provider, let it go through
+                    return mCandidate;
+                }
+                final ProviderInfo providerInfo = providers.get(0).providerInfo;
+                if (providerInfo == null || TextUtils.isEmpty(providerInfo.authority)) {
+                    // Bad provider - don't let candidate pass through.
+                    return null;
+                }
+                // Query candidate state (isComplete)
+                final Uri uri = new Uri.Builder()
+                        .scheme(ContentResolver.SCHEME_CONTENT)
+                        .authority(providerInfo.authority)
+                        .build();
+                final Bundle result = mContext.getContentResolver().call(
+                        uri, METHOD_GET_SUGGESTION_STATE, null /* args */,
+                        buildGetSuggestionStateExtras(mCandidate));
+                final boolean isComplete = result.getBoolean(RESULT_IS_COMPLETE, false);
+                Log.d(TAG, "Suggestion state result " + result);
+                return isComplete ? null : mCandidate;
+            }
+
+            @VisibleForTesting
+            static Bundle buildGetSuggestionStateExtras(CandidateSuggestion candidate) {
+                final Bundle args = new Bundle();
+                final String id = candidate.getId();
+                args.putString(EXTRA_CANDIDATE_ID, id);
+                args.putParcelable(EXTRA_COMPONENT_NAME, candidate.getComponent());
+                return args;
+            }
+        }
+    }
+}
diff --git a/src/com/android/settings/intelligence/suggestions/eligibility/DismissedChecker.java b/src/com/android/settings/intelligence/suggestions/eligibility/DismissedChecker.java
index a28ae3d..ba24baf 100644
--- a/src/com/android/settings/intelligence/suggestions/eligibility/DismissedChecker.java
+++ b/src/com/android/settings/intelligence/suggestions/eligibility/DismissedChecker.java
@@ -23,8 +23,8 @@
 import android.text.format.DateUtils;
 import android.util.Log;
 
-import com.android.settings.intelligence.suggestions.SuggestionDismissHandler;
-import com.android.settings.intelligence.suggestions.SuggestionService;
+import com.android.settings.intelligence.overlay.FeatureFactory;
+import com.android.settings.intelligence.suggestions.SuggestionFeatureProvider;
 
 public class DismissedChecker {
 
@@ -42,7 +42,8 @@
 
     // Shared prefs keys for storing dismissed state.
     // Index into current dismissed state.
-    private static final String SETUP_TIME = "_setup_time";
+    @VisibleForTesting
+    static final String SETUP_TIME = "_setup_time";
     // Default dismiss rule for suggestions.
 
     private static final int DEFAULT_FIRST_APPEAR_DAY = 0;
@@ -51,30 +52,37 @@
 
     public static boolean isEligible(Context context, String id, ResolveInfo info,
             boolean ignoreAppearRule) {
-        final SharedPreferences prefs = SuggestionService.getSharedPrefs(context);
-        final SuggestionDismissHandler dismissHandler = SuggestionDismissHandler.getInstance();
+        final SuggestionFeatureProvider featureProvider = FeatureFactory.get(context)
+                .suggestionFeatureProvider();
+        final SharedPreferences prefs = featureProvider.getSharedPrefs(context);
+        final long currentTimeMs = System.currentTimeMillis();
         final String keySetupTime = id + SETUP_TIME;
         if (!prefs.contains(keySetupTime)) {
             prefs.edit()
-                    .putLong(keySetupTime, System.currentTimeMillis())
+                    .putLong(keySetupTime, currentTimeMs)
                     .apply();
         }
 
         // Check if it's already manually dismissed
-        final boolean isDismissed = dismissHandler.isSuggestionDismissed(context, id);
+        final boolean isDismissed = featureProvider.isSuggestionDismissed(context, id);
         if (isDismissed) {
             return false;
         }
 
-        // Parse when suggestion should first appear. return true to artificially hide suggestion
-        // before then.
+        // Parse when suggestion should first appear. Hide suggestion before then.
         int firstAppearDay = ignoreAppearRule
                 ? DEFAULT_FIRST_APPEAR_DAY
                 : parseAppearDay(info);
-        long firstAppearDayInMs = getEndTime(prefs.getLong(keySetupTime, 0), firstAppearDay);
-        if (System.currentTimeMillis() >= firstAppearDayInMs) {
+        long setupTime = prefs.getLong(keySetupTime, 0);
+        if (setupTime > currentTimeMs) {
+            // SetupTime is the future, user's date/time is probably wrong at some point.
+            // Force setupTime to be now. So we get a more reasonable firstAppearDay.
+            setupTime = currentTimeMs;
+        }
+        final long firstAppearDayInMs = getFirstAppearTimeMillis(setupTime, firstAppearDay);
+        if (currentTimeMs >= firstAppearDayInMs) {
             // Dismiss timeout has passed, undismiss it.
-            dismissHandler.markSuggestionNotDismissed(context, id);
+            featureProvider.markSuggestionNotDismissed(context, id);
             return true;
         }
         return false;
@@ -104,8 +112,8 @@
         }
     }
 
-    private static long getEndTime(long startTime, int daysDelay) {
+    private static long getFirstAppearTimeMillis(long setupTime, int daysDelay) {
         long days = daysDelay * DateUtils.DAY_IN_MILLIS;
-        return startTime + days;
+        return setupTime + days;
     }
 }
diff --git a/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityChecker.java b/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityChecker.java
index 99efa4f..93cf8aa 100644
--- a/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityChecker.java
+++ b/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityChecker.java
@@ -48,8 +48,7 @@
     }
 
     private static boolean isEnabledInMetadata(Context context, String id, ResolveInfo info) {
-        final int isSupportedResource =
-                info.activityInfo.metaData.getInt(META_DATA_IS_SUPPORTED);
+        final int isSupportedResource = info.activityInfo.metaData.getInt(META_DATA_IS_SUPPORTED);
         try {
             final Resources res = context.getPackageManager()
                     .getResourcesForApplication(info.activityInfo.applicationInfo);
diff --git a/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestion.java b/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestion.java
index 09fd02d..a9ea657 100644
--- a/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestion.java
+++ b/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestion.java
@@ -17,17 +17,19 @@
 package com.android.settings.intelligence.suggestions.model;
 
 import android.app.PendingIntent;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
 import android.os.Bundle;
 import android.service.settings.suggestions.Suggestion;
+import android.support.annotation.VisibleForTesting;
 import android.text.TextUtils;
 import android.util.Log;
-import android.widget.RemoteViews;
 
 import com.android.settings.intelligence.suggestions.eligibility.AccountEligibilityChecker;
 import com.android.settings.intelligence.suggestions.eligibility.ConnectivityEligibilityChecker;
@@ -35,6 +37,8 @@
 import com.android.settings.intelligence.suggestions.eligibility.FeatureEligibilityChecker;
 import com.android.settings.intelligence.suggestions.eligibility.ProviderEligibilityChecker;
 
+import java.util.List;
+
 /**
  * A wrapper to {@link android.content.pm.ResolveInfo} that matches Suggestion signature.
  * <p/>
@@ -43,15 +47,20 @@
  */
 public class CandidateSuggestion {
 
-    public static final String META_DATA_PREFERENCE_ICON_TINTABLE =
-            "com.android.settings.icon_tintable";
+    private static final String TAG = "CandidateSuggestion";
 
+    /**
+     * Name of the meta-data item that should be set in the AndroidManifest.xml
+     * to specify the title text that should be displayed for the preference.
+     */
+    @VisibleForTesting
     public static final String META_DATA_PREFERENCE_TITLE = "com.android.settings.title";
 
     /**
      * Name of the meta-data item that should be set in the AndroidManifest.xml
      * to specify the summary text that should be displayed for the preference.
      */
+    @VisibleForTesting
     public static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary";
 
     /**
@@ -61,23 +70,28 @@
      *
      * Summary provided by the content provider overrides any static summary.
      */
+    @VisibleForTesting
     public static final String META_DATA_PREFERENCE_SUMMARY_URI =
             "com.android.settings.summary_uri";
 
-    public static final String META_DATA_PREFERENCE_CUSTOM_VIEW =
-            "com.android.settings.custom_view";
-
     /**
      * Name of the meta-data item that should be set in the AndroidManifest.xml
      * to specify the icon that should be displayed for the preference.
      */
+    @VisibleForTesting
     public static final String META_DATA_PREFERENCE_ICON = "com.android.settings.icon";
 
-    private static final String TAG = "CandidateSuggestion";
+    /**
+     * Hint for type of suggestion UI to be displayed.
+     */
+    @VisibleForTesting
+    public static final String META_DATA_PREFERENCE_CUSTOM_VIEW =
+            "com.android.settings.custom_view";
 
     private final String mId;
     private final Context mContext;
     private final ResolveInfo mResolveInfo;
+    private final ComponentName mComponent;
     private final Intent mIntent;
     private final boolean mIsEligible;
     private final boolean mIgnoreAppearRule;
@@ -87,8 +101,9 @@
         mContext = context;
         mIgnoreAppearRule = ignoreAppearRule;
         mResolveInfo = resolveInfo;
-        mIntent = new Intent()
-                .setClassName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name);
+        mIntent = new Intent().setClassName(
+                resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name);
+        mComponent = mIntent.getComponent();
         mId = generateId();
         mIsEligible = initIsEligible();
     }
@@ -97,6 +112,10 @@
         return mId;
     }
 
+    public ComponentName getComponent() {
+        return mComponent;
+    }
+
     /**
      * Whether or not this candidate is eligible for display.
      * <p/>
@@ -142,71 +161,121 @@
 
     private void updateBuilder(Suggestion.Builder builder) {
         final PackageManager pm = mContext.getPackageManager();
-        final ApplicationInfo applicationInfo = mResolveInfo.activityInfo.applicationInfo;
+        final String packageName = mComponent.getPackageName();
 
-        int icon = 0;
-        boolean iconTintable = false;
-        String title = null;
-        String summary = null;
-        RemoteViews remoteViews = null;
+        int iconRes = 0;
+        int flags = 0;
+        CharSequence title = null;
+        CharSequence summary = null;
+        Icon icon = null;
 
         // Get the activity's meta-data
         try {
-            final Resources res = pm.getResourcesForApplication(applicationInfo.packageName);
+            final Resources res = pm.getResourcesForApplication(packageName);
             final Bundle metaData = mResolveInfo.activityInfo.metaData;
 
             if (res != null && metaData != null) {
+                // First get override data
+                final Bundle overrideData = getOverrideData(metaData);
+                // Get icon
                 if (metaData.containsKey(META_DATA_PREFERENCE_ICON)) {
-                    icon = metaData.getInt(META_DATA_PREFERENCE_ICON);
+                    iconRes = metaData.getInt(META_DATA_PREFERENCE_ICON);
+                } else {
+                    iconRes = mResolveInfo.activityInfo.icon;
                 }
-                if (metaData.containsKey(META_DATA_PREFERENCE_ICON_TINTABLE)) {
-                    iconTintable = metaData.getBoolean(META_DATA_PREFERENCE_ICON_TINTABLE);
+                if (iconRes != 0) {
+                    icon = Icon.createWithResource(
+                            mResolveInfo.activityInfo.packageName, iconRes);
                 }
-                if (metaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
+                // Get title
+                title = getStringFromBundle(overrideData, META_DATA_PREFERENCE_TITLE);
+                if (TextUtils.isEmpty(title) && metaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
                     if (metaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) {
                         title = res.getString(metaData.getInt(META_DATA_PREFERENCE_TITLE));
                     } else {
                         title = metaData.getString(META_DATA_PREFERENCE_TITLE);
                     }
                 }
-                if (metaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) {
+                // Get summary
+                summary = getStringFromBundle(overrideData, META_DATA_PREFERENCE_SUMMARY);
+                if (TextUtils.isEmpty(summary)
+                        && metaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) {
                     if (metaData.get(META_DATA_PREFERENCE_SUMMARY) instanceof Integer) {
                         summary = res.getString(metaData.getInt(META_DATA_PREFERENCE_SUMMARY));
                     } else {
                         summary = metaData.getString(META_DATA_PREFERENCE_SUMMARY);
                     }
                 }
-                if (metaData.containsKey(META_DATA_PREFERENCE_CUSTOM_VIEW)) {
-                    int layoutId = metaData.getInt(META_DATA_PREFERENCE_CUSTOM_VIEW);
-                    remoteViews = new RemoteViews(applicationInfo.packageName, layoutId);
-                }
+                // Detect remote view
+                flags = metaData.containsKey(META_DATA_PREFERENCE_CUSTOM_VIEW)
+                        ? Suggestion.FLAG_HAS_BUTTON : 0;
             }
         } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
-            Log.d(TAG, "Couldn't find info", e);
+            Log.w(TAG, "Couldn't find info", e);
         }
 
         // Set the preference title to the activity's label if no
         // meta-data is found
         if (TextUtils.isEmpty(title)) {
-            title = mResolveInfo.activityInfo.loadLabel(pm).toString();
+            title = mResolveInfo.activityInfo.loadLabel(pm);
         }
-
-        if (icon == 0) {
-            icon = mResolveInfo.activityInfo.icon;
-        }
-        // TODO: Need to use ContentProvider to read dynamic title/summary etc.
-        final PendingIntent pendingIntent = PendingIntent
-                .getActivity(mContext, 0 /* requestCode */, mIntent, 0 /* flags */);
         builder.setTitle(title)
                 .setSummary(summary)
-                .setPendingIntent(pendingIntent);
-        // TODO: Need to extend Suggestion and set the following.
-        // set icon
-        // set icon tintable
-        // set remote view
+                .setFlags(flags)
+                .setIcon(icon)
+                .setPendingIntent(PendingIntent
+                        .getActivity(mContext, 0 /* requestCode */, mIntent, 0 /* flags */));
+    }
+
+    /**
+     * Extracts a string from bundle.
+     */
+    private CharSequence getStringFromBundle(Bundle bundle, String key) {
+        if (bundle == null || TextUtils.isEmpty(key)) {
+            return null;
+        }
+        return bundle.getString(key);
+    }
+
+    private Bundle getOverrideData(Bundle metadata) {
+        if (metadata == null || !metadata.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) {
+            Log.d(TAG, "Metadata null or has no info about summary_uri");
+            return null;
+        }
+
+        final String uriString = metadata.getString(META_DATA_PREFERENCE_SUMMARY_URI);
+        final Bundle bundle = getBundleFromUri(uriString);
+        return bundle;
+    }
+
+    /**
+     * Calls method through ContentProvider and expects a bundle in return.
+     */
+    private Bundle getBundleFromUri(String uriString) {
+        final Uri uri = Uri.parse(uriString);
+
+        final String method = getMethodFromUri(uri);
+        if (TextUtils.isEmpty(method)) {
+            return null;
+        }
+        return mContext.getContentResolver().call(uri, method, null /* args */, null /* bundle */);
+    }
+
+    /**
+     * Returns the first path segment of the uri if it exists as the method, otherwise null.
+     */
+    private String getMethodFromUri(Uri uri) {
+        if (uri == null) {
+            return null;
+        }
+        final List<String> pathSegments = uri.getPathSegments();
+        if ((pathSegments == null) || pathSegments.isEmpty()) {
+            return null;
+        }
+        return pathSegments.get(0);
     }
 
     private String generateId() {
-        return mIntent.getComponent().flattenToString();
+        return mComponent.flattenToString();
     }
 }
diff --git a/src/com/android/settings/intelligence/suggestions/ranking/EventStore.java b/src/com/android/settings/intelligence/suggestions/ranking/SuggestionEventStore.java
similarity index 64%
rename from src/com/android/settings/intelligence/suggestions/ranking/EventStore.java
rename to src/com/android/settings/intelligence/suggestions/ranking/SuggestionEventStore.java
index f79c513..1f5f7c0 100644
--- a/src/com/android/settings/intelligence/suggestions/ranking/EventStore.java
+++ b/src/com/android/settings/intelligence/suggestions/ranking/SuggestionEventStore.java
@@ -16,8 +16,11 @@
 
 package com.android.settings.intelligence.suggestions.ranking;
 
+import static android.support.annotation.VisibleForTesting.NONE;
+
 import android.content.Context;
 import android.content.SharedPreferences;
+import android.support.annotation.VisibleForTesting;
 import android.util.Log;
 
 import java.util.Arrays;
@@ -27,7 +30,7 @@
 /**
  * Copied from packages/apps/Settings/src/.../dashboard/suggestions/EventStore
  */
-public class EventStore {
+public class SuggestionEventStore {
     public static final String TAG = "SuggestionEventStore";
 
     public static final String EVENT_SHOWN = "shown";
@@ -36,30 +39,39 @@
     public static final String METRIC_LAST_EVENT_TIME = "last_event_time";
     public static final String METRIC_COUNT = "count";
 
-    private static final Set<String> EVENTS = new HashSet<String>(
+    private static SuggestionEventStore sEventStore;
+
+    private static final Set<String> EVENTS = new HashSet<>(
             Arrays.asList(new String[]{EVENT_SHOWN, EVENT_DISMISSED, EVENT_CLICKED}));
-    private static final Set<String> METRICS = new HashSet<String>(
+    private static final Set<String> METRICS = new HashSet<>(
             Arrays.asList(new String[]{METRIC_LAST_EVENT_TIME, METRIC_COUNT}));
 
     private final SharedPreferences mSharedPrefs;
 
-    public EventStore(Context context) {
+    public static SuggestionEventStore get(Context context) {
+        if (sEventStore == null) {
+            sEventStore = new SuggestionEventStore(context);
+        }
+        return sEventStore;
+    }
+
+    private SuggestionEventStore(Context context) {
         mSharedPrefs = context.getSharedPreferences(TAG, Context.MODE_PRIVATE);
     }
 
     /**
      * Writes individual log events.
      *
-     * @param pkgName:   Package for which this event is reported.
-     * @param eventType: Type of event (one of {@link #EVENTS}).
+     * @param suggestionId Package for which this event is reported.
+     * @param eventType    Type of event (one of {@link #EVENTS}).
      */
-    public void writeEvent(String pkgName, String eventType) {
+    public void writeEvent(String suggestionId, String eventType) {
         if (!EVENTS.contains(eventType)) {
             Log.w(TAG, "Reported event type " + eventType + " is not a valid type!");
             return;
         }
-        final String lastTimePrefKey = getPrefKey(pkgName, eventType, METRIC_LAST_EVENT_TIME);
-        final String countPrefKey = getPrefKey(pkgName, eventType, METRIC_COUNT);
+        final String lastTimePrefKey = getPrefKey(suggestionId, eventType, METRIC_LAST_EVENT_TIME);
+        final String countPrefKey = getPrefKey(suggestionId, eventType, METRIC_COUNT);
         writePref(lastTimePrefKey, System.currentTimeMillis());
         writePref(countPrefKey, readPref(countPrefKey, (long) 0) + 1);
     }
@@ -67,12 +79,12 @@
     /**
      * Reads metric of the the reported events (e.g., counts).
      *
-     * @param pkgName:    Package for which this metric is queried.
-     * @param eventType:  Type of event (one of {@link #EVENTS}).
-     * @param metricType: Type of the queried metric (one of {@link #METRICS}).
+     * @param suggestionId Suggestion Id for which this metric is queried.
+     * @param eventType    Type of event (one of {@link #EVENTS}).
+     * @param metricType   Type of the queried metric (one of {@link #METRICS}).
      * @return the corresponding metric.
      */
-    public long readMetric(String pkgName, String eventType, String metricType) {
+    public long readMetric(String suggestionId, String eventType, String metricType) {
         if (!EVENTS.contains(eventType)) {
             Log.w(TAG, "Reported event type " + eventType + " is not a valid event!");
             return 0;
@@ -80,7 +92,12 @@
             Log.w(TAG, "Required stat type + " + metricType + " is not a valid stat!");
             return 0;
         }
-        return readPref(getPrefKey(pkgName, eventType, metricType), (long) 0);
+        return readPref(getPrefKey(suggestionId, eventType, metricType), (long) 0);
+    }
+
+    @VisibleForTesting(otherwise = NONE)
+    public void clear() {
+        mSharedPrefs.edit().clear().apply();
     }
 
     private void writePref(String prefKey, long value) {
@@ -91,10 +108,10 @@
         return mSharedPrefs.getLong(prefKey, defaultValue);
     }
 
-    private String getPrefKey(String pkgName, String eventType, String statType) {
+    private String getPrefKey(String suggestionId, String eventType, String statType) {
         return new StringBuilder()
                 .append("setting_suggestion_")
-                .append(pkgName)
+                .append(suggestionId)
                 .append("_")
                 .append(eventType)
                 .append("_")
diff --git a/src/com/android/settings/intelligence/suggestions/ranking/SuggestionFeaturizer.java b/src/com/android/settings/intelligence/suggestions/ranking/SuggestionFeaturizer.java
index 9844268..07f1a88 100644
--- a/src/com/android/settings/intelligence/suggestions/ranking/SuggestionFeaturizer.java
+++ b/src/com/android/settings/intelligence/suggestions/ranking/SuggestionFeaturizer.java
@@ -16,6 +16,7 @@
 
 package com.android.settings.intelligence.suggestions.ranking;
 
+import android.content.Context;
 import android.service.settings.suggestions.Suggestion;
 
 import java.util.ArrayList;
@@ -47,16 +48,10 @@
     public static final double TIME_NORMALIZATION_FACTOR = 2e10;
     public static final double COUNT_NORMALIZATION_FACTOR = 500;
 
-    private final EventStore mEventStore;
+    private final SuggestionEventStore mEventStore;
 
-    /**
-     * Constructor
-     *
-     * @param eventStore An instance of {@code EventStore} which maintains the recorded suggestion
-     *                   events.
-     */
-    public SuggestionFeaturizer(EventStore eventStore) {
-        mEventStore = eventStore;
+    public SuggestionFeaturizer(Context context) {
+        mEventStore = SuggestionEventStore.get(context);
     }
 
     /**
@@ -68,19 +63,22 @@
     public Map<String, Map<String, Double>> featurize(List<Suggestion> suggestions) {
         Map<String, Map<String, Double>> features = new HashMap<>();
         Long curTimeMs = System.currentTimeMillis();
-        List<String> pkgNames = new ArrayList<>(suggestions.size());
+        final List<String> suggestionIds = new ArrayList<>(suggestions.size());
         for (Suggestion suggestion : suggestions) {
-            pkgNames.add(suggestion.getId());
+            suggestionIds.add(suggestion.getId());
         }
-        for (String pkgName : pkgNames) {
+        for (String id : suggestionIds) {
             Map<String, Double> featureMap = new HashMap<>();
-            features.put(pkgName, featureMap);
-            Long lastShownTime = mEventStore
-                    .readMetric(pkgName, EventStore.EVENT_SHOWN, EventStore.METRIC_LAST_EVENT_TIME);
-            Long lastDismissedTime = mEventStore.readMetric(pkgName, EventStore.EVENT_DISMISSED,
-                    EventStore.METRIC_LAST_EVENT_TIME);
-            Long lastClickedTime = mEventStore.readMetric(pkgName, EventStore.EVENT_CLICKED,
-                    EventStore.METRIC_LAST_EVENT_TIME);
+            features.put(id, featureMap);
+            Long lastShownTime = mEventStore.readMetric(id,
+                    SuggestionEventStore.EVENT_SHOWN,
+                    SuggestionEventStore.METRIC_LAST_EVENT_TIME);
+            Long lastDismissedTime = mEventStore.readMetric(id,
+                    SuggestionEventStore.EVENT_DISMISSED,
+                    SuggestionEventStore.METRIC_LAST_EVENT_TIME);
+            Long lastClickedTime = mEventStore.readMetric(id,
+                    SuggestionEventStore.EVENT_CLICKED,
+                    SuggestionEventStore.METRIC_LAST_EVENT_TIME);
             featureMap.put(FEATURE_IS_SHOWN, booleanToDouble(lastShownTime > 0));
             featureMap.put(FEATURE_IS_DISMISSED, booleanToDouble(lastDismissedTime > 0));
             featureMap.put(FEATURE_IS_CLICKED, booleanToDouble(lastClickedTime > 0));
@@ -90,12 +88,12 @@
                     normalizedTimeDiff(curTimeMs, lastDismissedTime));
             featureMap.put(FEATURE_TIME_FROM_LAST_CLICKED,
                     normalizedTimeDiff(curTimeMs, lastClickedTime));
-            featureMap.put(FEATURE_SHOWN_COUNT, normalizedCount(mEventStore
-                    .readMetric(pkgName, EventStore.EVENT_SHOWN, EventStore.METRIC_COUNT)));
-            featureMap.put(FEATURE_DISMISSED_COUNT, normalizedCount(mEventStore
-                    .readMetric(pkgName, EventStore.EVENT_DISMISSED, EventStore.METRIC_COUNT)));
-            featureMap.put(FEATURE_CLICKED_COUNT, normalizedCount(mEventStore
-                    .readMetric(pkgName, EventStore.EVENT_CLICKED, EventStore.METRIC_COUNT)));
+            featureMap.put(FEATURE_SHOWN_COUNT, normalizedCount(mEventStore.readMetric(id,
+                    SuggestionEventStore.EVENT_SHOWN, SuggestionEventStore.METRIC_COUNT)));
+            featureMap.put(FEATURE_DISMISSED_COUNT, normalizedCount(mEventStore.readMetric(id,
+                    SuggestionEventStore.EVENT_DISMISSED, SuggestionEventStore.METRIC_COUNT)));
+            featureMap.put(FEATURE_CLICKED_COUNT, normalizedCount(mEventStore.readMetric(id,
+                    SuggestionEventStore.EVENT_CLICKED, SuggestionEventStore.METRIC_COUNT)));
         }
         return features;
     }
diff --git a/src/com/android/settings/intelligence/suggestions/ranking/SuggestionRanker.java b/src/com/android/settings/intelligence/suggestions/ranking/SuggestionRanker.java
index 887f922..e62262e 100644
--- a/src/com/android/settings/intelligence/suggestions/ranking/SuggestionRanker.java
+++ b/src/com/android/settings/intelligence/suggestions/ranking/SuggestionRanker.java
@@ -20,6 +20,9 @@
 import android.service.settings.suggestions.Suggestion;
 import android.support.annotation.VisibleForTesting;
 
+import com.android.settings.intelligence.overlay.FeatureFactory;
+
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -30,6 +33,7 @@
  * Copied from packages/apps/Settings/src/.../dashboard/suggestions/SuggestionRanker
  */
 public class SuggestionRanker {
+
     private static final String TAG = "SuggestionRanker";
 
     // The following coefficients form a linear model, which mixes the features to obtain a
@@ -46,39 +50,48 @@
         put(SuggestionFeaturizer.FEATURE_SHOWN_COUNT, -2.35993512546);
     }};
 
-    private static SuggestionRanker sInstance;
-
+    private final long mMaxSuggestionsDisplayCount;
     private final SuggestionFeaturizer mSuggestionFeaturizer;
-    private final Map<Suggestion, Double> relevanceMetrics;
+    private final Map<Suggestion, Double> mRelevanceMetrics;
 
     Comparator<Suggestion> suggestionComparator = new Comparator<Suggestion>() {
         @Override
         public int compare(Suggestion suggestion1, Suggestion suggestion2) {
-            return relevanceMetrics.get(suggestion1) < relevanceMetrics.get(suggestion2) ? 1 : -1;
+            return mRelevanceMetrics.get(suggestion1) < mRelevanceMetrics.get(suggestion2) ? 1 : -1;
         }
     };
 
-    @VisibleForTesting
-    SuggestionRanker(SuggestionFeaturizer suggestionFeaturizer) {
+    public SuggestionRanker(Context context, SuggestionFeaturizer suggestionFeaturizer) {
         mSuggestionFeaturizer = suggestionFeaturizer;
-        relevanceMetrics = new HashMap<>();
+        mRelevanceMetrics = new HashMap<>();
+        mMaxSuggestionsDisplayCount = FeatureFactory.get(context).experimentFeatureProvider()
+                .getMaxSuggestionDisplayCount(context);
     }
 
-    public static SuggestionRanker getInstance(Context context) {
-        if (sInstance == null) {
-            sInstance = new SuggestionRanker(
-                    new SuggestionFeaturizer(new EventStore(context.getApplicationContext())));
-        }
-        return sInstance;
-    }
-
-    public void rankSuggestions(List<Suggestion> suggestions) {
-        relevanceMetrics.clear();
+    /**
+     * Filter out suggestions that are not relevant at the moment, and rank the rest.
+     *
+     * @return a list of suggestion ranked by relevance.
+     */
+    public List<Suggestion> rankRelevantSuggestions(List<Suggestion> suggestions) {
+        mRelevanceMetrics.clear();
         Map<String, Map<String, Double>> features = mSuggestionFeaturizer.featurize(suggestions);
         for (Suggestion suggestion : suggestions) {
-            relevanceMetrics.put(suggestion, getRelevanceMetric(features.get(suggestion.getId())));
+            mRelevanceMetrics.put(suggestion, getRelevanceMetric(features.get(suggestion.getId())));
         }
-        Collections.sort(suggestions, suggestionComparator);
+        final List<Suggestion> rankedSuggestions = new ArrayList<>();
+        rankedSuggestions.addAll(suggestions);
+        Collections.sort(rankedSuggestions, suggestionComparator);
+
+        if (rankedSuggestions.size() < mMaxSuggestionsDisplayCount) {
+            return rankedSuggestions;
+        } else {
+            final List<Suggestion> relevantSuggestions = new ArrayList<>();
+            for (int i = 0; i < mMaxSuggestionsDisplayCount; i++) {
+                relevantSuggestions.add(rankedSuggestions.get(i));
+            }
+            return relevantSuggestions;
+        }
     }
 
     @VisibleForTesting
diff --git a/src/com/android/settings/intelligence/utils/AsyncLoader.java b/src/com/android/settings/intelligence/utils/AsyncLoader.java
new file mode 100644
index 0000000..54b62b6
--- /dev/null
+++ b/src/com/android/settings/intelligence/utils/AsyncLoader.java
@@ -0,0 +1,92 @@
+package com.android.settings.intelligence.utils;
+
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+
+/**
+ * This class fills in some boilerplate for AsyncTaskLoader to actually load things.
+ *
+ * Subclasses need to implement {@link AsyncLoader#loadInBackground()} to perform the actual
+ * background task, and {@link AsyncLoader#onDiscardResult(T)} to clean up previously loaded
+ * results.
+ *
+ * This loader is based on the MailAsyncTaskLoader from the AOSP EmailUnified repo.
+ */
+public abstract class AsyncLoader<T> extends AsyncTaskLoader<T> {
+    private T mResult;
+
+    public AsyncLoader(final Context context) {
+        super(context);
+    }
+
+    @Override
+    protected void onStartLoading() {
+        if (mResult != null) {
+            deliverResult(mResult);
+        }
+
+        if (takeContentChanged() || mResult == null) {
+            forceLoad();
+        }
+    }
+
+    @Override
+    protected void onStopLoading() {
+        cancelLoad();
+    }
+
+    @Override
+    public void deliverResult(final T data) {
+        if (isReset()) {
+            if (data != null) {
+                onDiscardResult(data);
+            }
+            return;
+        }
+
+        final T oldResult = mResult;
+        mResult = data;
+
+        if (isStarted()) {
+            super.deliverResult(data);
+        }
+
+        if (oldResult != null && oldResult != mResult) {
+            onDiscardResult(oldResult);
+        }
+    }
+
+    @Override
+    protected void onReset() {
+        super.onReset();
+
+        onStopLoading();
+
+        if (mResult != null) {
+            onDiscardResult(mResult);
+        }
+        mResult = null;
+    }
+
+    @Override
+    public void onCanceled(final T data) {
+        super.onCanceled(data);
+
+        if (data != null) {
+            onDiscardResult(data);
+        }
+    }
+
+    /**
+     * Called when discarding the load results so subclasses can take care of clean-up or
+     * recycling tasks. This is not called if the same result (by way of pointer equality) is
+     * returned again by a subsequent call to loadInBackground, or if result is null.
+     *
+     * Note that this may be called concurrently with loadInBackground(), and in some circumstances
+     * may be called more than once for a given object.
+     *
+     * @param result The value returned from {@link AsyncLoader#loadInBackground()} which
+     *               is to be discarded.
+     */
+    protected abstract void onDiscardResult(final T result);
+}
\ No newline at end of file
diff --git a/tests/Android.mk b/tests/Android.mk
deleted file mode 100644
index fd297e3..0000000
--- a/tests/Android.mk
+++ /dev/null
@@ -1,5 +0,0 @@
-LOCAL_PATH := $(call my-dir)
-include $(CLEAR_VARS)
-
-# Include all makefiles in subdirectories
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tests/robotests/Android.mk b/tests/robotests/Android.mk
deleted file mode 100644
index c8ec19f..0000000
--- a/tests/robotests/Android.mk
+++ /dev/null
@@ -1,40 +0,0 @@
-#############################################
-# Turbo Robolectric test target. #
-#############################################
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-# Include the testing libraries (JUnit4 + Robolectric libs).
-LOCAL_STATIC_JAVA_LIBRARIES := \
-    mockito-robolectric-prebuilt \
-    truth-prebuilt
-
-LOCAL_JAVA_LIBRARIES := \
-    junit \
-    platform-robolectric-3.4.2-prebuilt \
-    sdk_vcurrent
-
-LOCAL_INSTRUMENTATION_FOR := SettingsIntelligence
-LOCAL_MODULE := SettingsIntelligenceRoboTests
-
-LOCAL_MODULE_TAGS := optional
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-#############################################################
-# Turbo runner target to run the previous target. #
-#############################################################
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := RunSettingsIntelligenceRoboTests
-
-LOCAL_SDK_VERSION := system_current
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
-    SettingsIntelligenceRoboTests
-
-LOCAL_TEST_PACKAGE := SettingsIntelligence
-
-include prebuilts/misc/common/robolectric/3.4.2/run_robotests.mk
diff --git a/tests/robotests/src/android/service/settings/suggestions/Suggestion.java b/tests/robotests/src/android/service/settings/suggestions/Suggestion.java
deleted file mode 100644
index f670f89..0000000
--- a/tests/robotests/src/android/service/settings/suggestions/Suggestion.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * 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 android.service.settings.suggestions;
-
-import android.app.PendingIntent;
-import android.os.Parcel;
-import android.text.TextUtils;
-
-public class Suggestion {
-    private final String mId;
-    private final CharSequence mTitle;
-    private final CharSequence mSummary;
-    private final PendingIntent mPendingIntent;
-
-    /**
-     * Gets the id for the suggestion object.
-     */
-    public String getId() {
-        return mId;
-    }
-
-    /**
-     * Title of the suggestion that is shown to the user.
-     */
-    public CharSequence getTitle() {
-        return mTitle;
-    }
-
-    /**
-     * Optional summary describing what this suggestion controls.
-     */
-    public CharSequence getSummary() {
-        return mSummary;
-    }
-
-    /**
-     * The Intent to launch when the suggestion is activated.
-     */
-    public PendingIntent getPendingIntent() {
-        return mPendingIntent;
-    }
-
-    private Suggestion(Builder builder) {
-        mTitle = builder.mTitle;
-        mSummary = builder.mSummary;
-        mPendingIntent = builder.mPendingIntent;
-        mId = builder.mId;
-    }
-
-    /**
-     * Builder class for {@link Suggestion}.
-     */
-    public static class Builder {
-        private final String mId;
-        private CharSequence mTitle;
-        private CharSequence mSummary;
-        private PendingIntent mPendingIntent;
-
-        public Builder(String id) {
-            if (TextUtils.isEmpty(id)) {
-                throw new IllegalArgumentException("Suggestion id cannot be empty");
-            }
-            mId = id;
-        }
-
-        /**
-         * Sets suggestion title
-         */
-
-        public Builder setTitle(CharSequence title) {
-            mTitle = title;
-            return this;
-        }
-
-        /**
-         * Sets suggestion summary
-         */
-        public Builder setSummary(CharSequence summary) {
-            mSummary = summary;
-            return this;
-        }
-
-        /**
-         * Sets suggestion intent
-         */
-        public Builder setPendingIntent(PendingIntent pendingIntent) {
-            mPendingIntent = pendingIntent;
-            return this;
-        }
-
-        /**
-         * Builds an immutable {@link Suggestion} object.
-         */
-        public Suggestion build() {
-            return new Suggestion(this /* builder */);
-        }
-    }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/SuggestionParserTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/SuggestionParserTest.java
deleted file mode 100644
index c993b6b..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/SuggestionParserTest.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * 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.suggestions;
-
-import static com.android.settings.intelligence.suggestions.model.CandidateSuggestionTest.newInfo;
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ResolveInfo;
-import android.service.settings.suggestions.Suggestion;
-
-import com.android.settings.intelligence.TestConfig;
-import com.android.settings.intelligence.suggestions.model.SuggestionCategoryRegistry;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.Shadows;
-import org.robolectric.annotation.Config;
-import org.robolectric.shadows.ShadowPackageManager;
-
-import java.util.List;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class SuggestionParserTest {
-
-    private Context mContext;
-    private ShadowPackageManager mPackageManager;
-    private SuggestionParser mSuggestionParser;
-    private ResolveInfo mInfo1;
-    private ResolveInfo mInfo1Dupe;
-    private ResolveInfo mInfo2;
-    private ResolveInfo mInfo3;
-    private ResolveInfo mInfo4;
-
-    private Intent exclusiveIntent1;
-    private Intent exclusiveIntent2;
-    private Intent regularIntent;
-
-    @Before
-    public void setUp() {
-        mContext = RuntimeEnvironment.application;
-        mPackageManager = Shadows.shadowOf(mContext.getPackageManager());
-        mSuggestionParser = new SuggestionParser(mContext);
-
-        mInfo1 = newInfo(mContext, "Class1", true /* systemApp */,
-                null /* summaryUri */, "title4", 0 /* titleResId */);
-        mInfo1Dupe = newInfo(mContext, "Class1", true /* systemApp */,
-                null /* summaryUri */, "title4", 0 /* titleResId */);
-        mInfo2 = newInfo(mContext, "Class2", true /* systemApp */,
-                null /* summaryUri */, "title4", 0 /* titleResId */);
-        mInfo3 = newInfo(mContext, "Class3", true /* systemApp */,
-                null /* summaryUri */, "title4", 0 /* titleResId */);
-        mInfo4 = newInfo(mContext, "Class4", true /* systemApp */,
-                null /* summaryUri */, "title4", 0 /* titleResId */);
-        mInfo4.activityInfo.applicationInfo.packageName = "ineligible";
-
-        exclusiveIntent1 = new Intent(Intent.ACTION_MAIN).addCategory(
-                SuggestionCategoryRegistry.CATEGORIES.get(0).getCategory());
-        exclusiveIntent2 = new Intent(Intent.ACTION_MAIN).addCategory(
-                SuggestionCategoryRegistry.CATEGORIES.get(1).getCategory());
-        regularIntent = new Intent(Intent.ACTION_MAIN).addCategory(
-                SuggestionCategoryRegistry.CATEGORIES.get(2).getCategory());
-    }
-
-    @Test
-    public void testGetSuggestions_exclusive() {
-        mPackageManager.addResolveInfoForIntent(exclusiveIntent1, mInfo1);
-        mPackageManager.addResolveInfoForIntent(exclusiveIntent1, mInfo1Dupe);
-        mPackageManager.addResolveInfoForIntent(exclusiveIntent2, mInfo2);
-        mPackageManager.addResolveInfoForIntent(regularIntent, mInfo3);
-        final List<Suggestion> suggestions = mSuggestionParser.getSuggestions();
-
-        // info1
-        assertThat(suggestions).hasSize(1);
-    }
-
-    @Test
-    public void testGetSuggestion_onlyRegularCategoryAndNoDupe() {
-        mPackageManager.addResolveInfoForIntent(regularIntent, mInfo1);
-        mPackageManager.addResolveInfoForIntent(regularIntent, mInfo1Dupe);
-        mPackageManager.addResolveInfoForIntent(regularIntent, mInfo2);
-        mPackageManager.addResolveInfoForIntent(regularIntent, mInfo3);
-        mPackageManager.addResolveInfoForIntent(regularIntent, mInfo4);
-
-        final List<Suggestion> suggestions = mSuggestionParser.getSuggestions();
-
-        // info1, info2, info3 (info4 is skip because its package name is ineligible)
-        assertThat(suggestions).hasSize(3);
-    }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/SuggestionServiceTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/SuggestionServiceTest.java
deleted file mode 100644
index 5ed7665..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/SuggestionServiceTest.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * 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.suggestions;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.service.settings.suggestions.Suggestion;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.Robolectric;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.android.controller.ServiceController;
-import org.robolectric.annotation.Config;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class SuggestionServiceTest {
-
-    private SuggestionService mService;
-    private ServiceController<SuggestionService> mServiceController;
-
-    @Before
-    public void setUp() {
-        mServiceController = Robolectric.buildService(SuggestionService.class);
-        mService = mServiceController.create().get();
-    }
-
-    @Test
-    public void getSuggestion_shouldReturnNonNull() {
-        assertThat(mService.onGetSuggestions()).isNotNull();
-    }
-
-    @Test
-    public void dismissSuggestion_shouldDismiss() {
-        final String id = "id1";
-        final Suggestion suggestion = new Suggestion.Builder(id).build();
-
-        // Not dismissed
-        assertThat(SuggestionDismissHandler.getInstance().isSuggestionDismissed(mService, id))
-                .isFalse();
-
-        // Dismiss
-        mService.onSuggestionDismissed(suggestion);
-
-        // Dismissed
-        assertThat(SuggestionDismissHandler.getInstance().isSuggestionDismissed(mService, id))
-                .isTrue();
-    }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/AccountEligibilityCheckerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/AccountEligibilityCheckerTest.java
deleted file mode 100644
index cfec40a..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/AccountEligibilityCheckerTest.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * 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.suggestions.eligibility;
-
-import static com.android.settings.intelligence.suggestions.eligibility.AccountEligibilityChecker
-        .META_DATA_REQUIRE_ACCOUNT;
-import static com.google.common.truth.Truth.assertThat;
-
-import android.accounts.Account;
-import android.accounts.AccountManager;
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.ResolveInfo;
-import android.os.Bundle;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.Shadows;
-import org.robolectric.annotation.Config;
-import org.robolectric.shadows.ShadowAccountManager;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class AccountEligibilityCheckerTest {
-
-    private static final String ID = "test";
-    private Context mContext;
-    private ResolveInfo mInfo;
-
-    @Before
-    public void setUp() {
-        mContext = RuntimeEnvironment.application;
-        mInfo = new ResolveInfo();
-        mInfo.activityInfo = new ActivityInfo();
-        mInfo.activityInfo.metaData = new Bundle();
-        mInfo.activityInfo.applicationInfo = new ApplicationInfo();
-        mInfo.activityInfo.applicationInfo.packageName =
-                RuntimeEnvironment.application.getPackageName();
-    }
-
-    @After
-    public void tearDown() {
-        final ShadowAccountManager shadowAccountManager = Shadows.shadowOf(
-                AccountManager.get(mContext));
-        shadowAccountManager.removeAllAccounts();
-    }
-
-    @Test
-    public void isEligible_noAccountRequirement_shouldReturnTrue() {
-        assertThat(AccountEligibilityChecker.isEligible(mContext, ID, mInfo))
-                .isTrue();
-    }
-
-    @Test
-    public void isEligible_failRequirement_shouldReturnFalse() {
-        // Require android.com account but AccountManager doesn't have it.
-        mInfo.activityInfo.metaData.putString(META_DATA_REQUIRE_ACCOUNT, "android.com");
-
-        assertThat(AccountEligibilityChecker.isEligible(mContext, ID, mInfo))
-                .isFalse();
-    }
-
-    @Test
-    public void isEligible_passRequirement_shouldReturnTrue() {
-        mInfo.activityInfo.metaData.putString(META_DATA_REQUIRE_ACCOUNT, "android.com");
-        final Account account = new Account("TEST", "android.com");
-
-        final ShadowAccountManager shadowAccountManager = Shadows.shadowOf(
-                AccountManager.get(mContext));
-        shadowAccountManager.addAccount(account);
-
-        assertThat(AccountEligibilityChecker.isEligible(mContext, ID, mInfo))
-                .isTrue();
-    }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/ConnectivityEligibilityCheckerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/ConnectivityEligibilityCheckerTest.java
deleted file mode 100644
index 42949d3..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/ConnectivityEligibilityCheckerTest.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * 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.suggestions.eligibility;
-
-import static com.android.settings.intelligence.suggestions.eligibility
-        .ConnectivityEligibilityChecker.META_DATA_IS_CONNECTION_REQUIRED;
-import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.ResolveInfo;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.os.Bundle;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.Shadows;
-import org.robolectric.annotation.Config;
-import org.robolectric.shadows.ShadowConnectivityManager;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class ConnectivityEligibilityCheckerTest {
-
-    private static final String ID = "test";
-    private Context mContext;
-    private ResolveInfo mInfo;
-    private ShadowConnectivityManager mConnectivityManager;
-
-    @Before
-    public void setUp() {
-        mContext = RuntimeEnvironment.application;
-        mInfo = new ResolveInfo();
-        mInfo.activityInfo = new ActivityInfo();
-        mInfo.activityInfo.metaData = new Bundle();
-        mInfo.activityInfo.applicationInfo = new ApplicationInfo();
-        mInfo.activityInfo.applicationInfo.packageName =
-                RuntimeEnvironment.application.getPackageName();
-
-        mConnectivityManager = Shadows.shadowOf(
-                (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE));
-    }
-
-    @After
-    public void tearDown() {
-        mConnectivityManager.setActiveNetworkInfo(null);
-    }
-
-    @Test
-    public void isEligible_noRequirement_shouldReturnTrue() {
-        assertThat(ConnectivityEligibilityChecker.isEligible(mContext, ID, mInfo))
-                .isTrue();
-
-        mInfo.activityInfo.metaData.putBoolean(META_DATA_IS_CONNECTION_REQUIRED, false);
-        assertThat(ConnectivityEligibilityChecker.isEligible(mContext, ID, mInfo))
-                .isTrue();
-    }
-
-    @Test
-    public void isEligible_hasConnection_shouldReturnTrue() {
-        mInfo.activityInfo.metaData.putBoolean(META_DATA_IS_CONNECTION_REQUIRED, true);
-
-        final NetworkInfo networkInfo = mock(NetworkInfo.class);
-        when(networkInfo.isConnectedOrConnecting())
-                .thenReturn(true);
-
-        mConnectivityManager.setActiveNetworkInfo(networkInfo);
-
-        assertThat(ConnectivityEligibilityChecker.isEligible(mContext, ID, mInfo))
-                .isTrue();
-    }
-
-    @Test
-    public void isEligible_noConnection_shouldReturnFalse() {
-        mInfo.activityInfo.metaData.putBoolean(META_DATA_IS_CONNECTION_REQUIRED, true);
-
-        mConnectivityManager.setActiveNetworkInfo(null);
-
-        assertThat(ConnectivityEligibilityChecker.isEligible(mContext, ID, mInfo))
-                .isFalse();
-    }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/DismissedCheckerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/DismissedCheckerTest.java
deleted file mode 100644
index f172553..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/DismissedCheckerTest.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * 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.suggestions.eligibility;
-
-import static com.android.settings.intelligence.suggestions.eligibility.DismissedChecker
-        .META_DATA_DISMISS_CONTROL;
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.ResolveInfo;
-import android.os.Bundle;
-
-import com.android.settings.intelligence.TestConfig;
-import com.android.settings.intelligence.suggestions.SuggestionDismissHandler;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class DismissedCheckerTest {
-    private static final String ID = "test";
-    private Context mContext;
-    private ResolveInfo mInfo;
-
-    @Before
-    public void setUp() {
-        mContext = RuntimeEnvironment.application;
-        mInfo = new ResolveInfo();
-        mInfo.activityInfo = new ActivityInfo();
-        mInfo.activityInfo.metaData = new Bundle();
-        mInfo.activityInfo.applicationInfo = new ApplicationInfo();
-        mInfo.activityInfo.applicationInfo.packageName =
-                RuntimeEnvironment.application.getPackageName();
-    }
-
-    @Test
-    public void isEligible_newSuggestion_noRule_shouldReturnTrue() {
-        assertThat(DismissedChecker.isEligible(mContext, ID, mInfo, true /* ignoreAppearRule */))
-                .isTrue();
-    }
-
-    @Test
-    public void isEligible_newSuggestion_hasFutureRule_shouldReturnFalse() {
-        mInfo.activityInfo.metaData.putString(META_DATA_DISMISS_CONTROL, "10");
-
-        assertThat(DismissedChecker.isEligible(mContext, ID, mInfo, false /* ignoreAppearRule */))
-                .isFalse();
-    }
-
-    @Test
-    public void isEligible_newSuggestion_ignoreFutureRule_shouldReturnFalse() {
-        mInfo.activityInfo.metaData.putString(META_DATA_DISMISS_CONTROL, "10");
-
-        assertThat(DismissedChecker.isEligible(mContext, ID, mInfo, true /* ignoreAppearRule */))
-                .isTrue();
-    }
-
-    @Test
-    public void isEligible_newSuggestion_hasPastRule_shouldReturnTrue() {
-        mInfo.activityInfo.metaData.putString(META_DATA_DISMISS_CONTROL, "-10");
-
-        assertThat(DismissedChecker.isEligible(mContext, ID, mInfo, false /* ignoreAppearRule */))
-                .isTrue();
-        assertThat(DismissedChecker.isEligible(mContext, ID, mInfo, true /* ignoreAppearRule */))
-                .isTrue();
-    }
-
-    @Test
-    public void isEligible_dismissedSuggestion_shouldReturnFalse() {
-        SuggestionDismissHandler.getInstance().markSuggestionDismissed(mContext, ID);
-
-        assertThat(DismissedChecker.isEligible(mContext, ID, mInfo, true /* ignoreAppearRule */))
-                .isFalse();
-    }
-
-}
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/FeatureEligibilityCheckerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/FeatureEligibilityCheckerTest.java
deleted file mode 100644
index 6c92e92..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/FeatureEligibilityCheckerTest.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * 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.suggestions.eligibility;
-
-
-import static com.android.settings.intelligence.suggestions.eligibility.FeatureEligibilityChecker
-        .META_DATA_REQUIRE_FEATURE;
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.FeatureInfo;
-import android.content.pm.ResolveInfo;
-import android.os.Bundle;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.Shadows;
-import org.robolectric.annotation.Config;
-import org.robolectric.shadows.ShadowPackageManager;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class FeatureEligibilityCheckerTest {
-
-    private static final String ID = "test";
-    private Context mContext;
-    private ResolveInfo mInfo;
-    private ShadowPackageManager mPackageManager;
-
-    @Before
-    public void setUp() {
-        mContext = RuntimeEnvironment.application;
-        mInfo = new ResolveInfo();
-        mInfo.activityInfo = new ActivityInfo();
-        mInfo.activityInfo.metaData = new Bundle();
-        mInfo.activityInfo.applicationInfo = new ApplicationInfo();
-        mInfo.activityInfo.applicationInfo.packageName =
-                RuntimeEnvironment.application.getPackageName();
-        mPackageManager = Shadows.shadowOf(mContext.getPackageManager());
-    }
-
-    @After
-    public void tearDown() {
-        mPackageManager.clearSystemAvailableFeatures();
-    }
-
-    @Test
-    public void isEligible_noRequirement_shouldReturnTrue() {
-        assertThat(FeatureEligibilityChecker.isEligible(mContext, ID, mInfo))
-                .isTrue();
-    }
-
-    @Test
-    public void isEligible_failRequirement_shouldReturnFalse() {
-        mInfo.activityInfo.metaData.putString(META_DATA_REQUIRE_FEATURE, "test_feature");
-
-        assertThat(FeatureEligibilityChecker.isEligible(mContext, ID, mInfo))
-                .isFalse();
-    }
-
-    @Test
-    public void isEligible_passRequirement_shouldReturnTrue() {
-        final FeatureInfo featureInfo = new FeatureInfo();
-        featureInfo.name = "fingerprint";
-        mInfo.activityInfo.metaData.putString(META_DATA_REQUIRE_FEATURE, featureInfo.name);
-        mPackageManager.addSystemAvailableFeature(featureInfo);
-
-        assertThat(FeatureEligibilityChecker.isEligible(mContext, ID, mInfo))
-                .isFalse();
-    }
-}
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityCheckerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityCheckerTest.java
deleted file mode 100644
index b4c0efd..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityCheckerTest.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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.suggestions.eligibility;
-
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.ResolveInfo;
-import android.os.Bundle;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class ProviderEligibilityCheckerTest {
-
-    private static final String ID = "test";
-    private Context mContext;
-    private ResolveInfo mInfo;
-
-    @Before
-    public void setUp() {
-        mContext = RuntimeEnvironment.application;
-        mInfo = new ResolveInfo();
-        mInfo.activityInfo = new ActivityInfo();
-        mInfo.activityInfo.metaData = new Bundle();
-        mInfo.activityInfo.applicationInfo = new ApplicationInfo();
-        mInfo.activityInfo.applicationInfo.packageName =
-                RuntimeEnvironment.application.getPackageName();
-    }
-
-    @Test
-    public void isEligible_systemFlagSet_shouldReturnTrue() {
-        mInfo.activityInfo.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
-
-        assertThat(ProviderEligibilityChecker.isEligible(mContext, ID, mInfo))
-                .isTrue();
-    }
-
-    @Test
-    public void isEligible_systemFlagNotSet_shouldReturnFalse() {
-        mInfo.activityInfo.applicationInfo.flags &= ~ApplicationInfo.FLAG_SYSTEM;
-
-        assertThat(ProviderEligibilityChecker.isEligible(mContext, ID, mInfo))
-                .isFalse();
-    }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestionTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestionTest.java
deleted file mode 100644
index 87c4440..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestionTest.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * 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.suggestions.model;
-
-import static com.android.settings.intelligence.suggestions.model.CandidateSuggestion
-        .META_DATA_PREFERENCE_ICON;
-import static com.android.settings.intelligence.suggestions.model.CandidateSuggestion
-        .META_DATA_PREFERENCE_SUMMARY;
-import static com.android.settings.intelligence.suggestions.model.CandidateSuggestion
-        .META_DATA_PREFERENCE_SUMMARY_URI;
-import static com.android.settings.intelligence.suggestions.model.CandidateSuggestion
-        .META_DATA_PREFERENCE_TITLE;
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.ResolveInfo;
-import android.os.Bundle;
-import android.service.settings.suggestions.Suggestion;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class CandidateSuggestionTest {
-
-    private static final String PACKAGE_NAME = "pkg";
-    private static final String CLASS_NAME = "class";
-
-    private Context mContext;
-    private ResolveInfo mInfo;
-
-    @Before
-    public void setUp() {
-        mContext = RuntimeEnvironment.application;
-        mInfo = new ResolveInfo();
-        mInfo.activityInfo = new ActivityInfo();
-        mInfo.activityInfo.metaData = new Bundle();
-        mInfo.activityInfo.packageName = PACKAGE_NAME;
-        mInfo.activityInfo.name = CLASS_NAME;
-
-        mInfo.activityInfo.applicationInfo = new ApplicationInfo();
-        mInfo.activityInfo.applicationInfo.packageName =
-                RuntimeEnvironment.application.getPackageName();
-    }
-
-    @Test
-    public void getId_shouldUseComponentName() {
-        final CandidateSuggestion candidate =
-                new CandidateSuggestion(mContext, mInfo, true /* ignoreAppearRule */);
-
-        assertThat(candidate.getId())
-                .contains(PACKAGE_NAME + "/" + CLASS_NAME);
-    }
-
-    @Test
-    public void parseMetadata_eligibleSuggestion() {
-        final ResolveInfo info = newInfo(mContext, "class", true /* systemApp */,
-                null /*summaryUri */, "title", 0 /* titleResId */);
-        Suggestion suggestion = new CandidateSuggestion(
-                mContext, info, false /* ignoreAppearRule*/)
-                .toSuggestion();
-        assertThat(suggestion.getId()).isEqualTo(mContext.getPackageName() + "/class");
-        assertThat(suggestion.getTitle()).isEqualTo("title");
-        assertThat(suggestion.getSummary()).isEqualTo("static-summary");
-    }
-
-    @Test
-    public void parseMetadata_ineligibleSuggestion() {
-        final ResolveInfo info = newInfo(mContext, "class", false /* systemApp */,
-                null /*summaryUri */, "title", 0 /* titleResId */);
-        final CandidateSuggestion candidate = new CandidateSuggestion(
-                mContext, info, false /* ignoreAppearRule*/);
-
-        assertThat(candidate.isEligible()).isFalse();
-        assertThat(candidate.toSuggestion()).isNull();
-    }
-
-    public static ResolveInfo newInfo(Context context, String className, boolean systemApp,
-            String summaryUri, String title, int titleResId) {
-        final ResolveInfo info = new ResolveInfo();
-        info.activityInfo = new ActivityInfo();
-        info.activityInfo.applicationInfo = new ApplicationInfo();
-        if (systemApp) {
-            info.activityInfo.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
-        }
-        info.activityInfo.packageName = context.getPackageName();
-        info.activityInfo.applicationInfo.packageName = info.activityInfo.packageName;
-        info.activityInfo.name = className;
-        info.activityInfo.metaData = new Bundle();
-        info.activityInfo.metaData.putInt(META_DATA_PREFERENCE_ICON, 314159);
-        info.activityInfo.metaData.putString(META_DATA_PREFERENCE_SUMMARY, "static-summary");
-        if (summaryUri != null) {
-            info.activityInfo.metaData.putString(META_DATA_PREFERENCE_SUMMARY_URI, summaryUri);
-        }
-        if (titleResId != 0) {
-            info.activityInfo.metaData.putInt(META_DATA_PREFERENCE_TITLE, titleResId);
-        } else if (title != null) {
-            info.activityInfo.metaData.putString(META_DATA_PREFERENCE_TITLE, title);
-        }
-        return info;
-    }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/model/SuggestionCategoryRegistryTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/model/SuggestionCategoryRegistryTest.java
deleted file mode 100644
index 3069ae8..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/model/SuggestionCategoryRegistryTest.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.suggestions.model;
-
-import static com.android.settings.intelligence.suggestions.model.SuggestionCategoryRegistry
-        .CATEGORIES;
-import static com.google.common.truth.Truth.assertThat;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class SuggestionCategoryRegistryTest {
-
-    @Test
-    public void getCategories_shouldHave10Categories() {
-        assertThat(CATEGORIES)
-                .hasSize(10);
-    }
-
-    @Test
-    public void verifyExclusiveCategories() {
-        final List<String> exclusiveCategories = new ArrayList<>();
-        exclusiveCategories.add(SuggestionCategoryRegistry.CATEGORY_KEY_DEFERRED_SETUP);
-        exclusiveCategories.add(SuggestionCategoryRegistry.CATEGORY_KEY_FIRST_IMPRESSION);
-
-        int exclusiveCount = 0;
-        for (SuggestionCategory category : CATEGORIES) {
-            if (category.isExclusive()) {
-                exclusiveCount++;
-                assertThat(exclusiveCategories).contains(category.getCategory());
-            }
-        }
-        assertThat(exclusiveCount).isEqualTo(exclusiveCategories.size());
-    }
-
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/model/SuggestionListBuilderTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/model/SuggestionListBuilderTest.java
deleted file mode 100644
index 47eda56..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/model/SuggestionListBuilderTest.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * 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.suggestions.model;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.service.settings.suggestions.Suggestion;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-
-import java.util.Arrays;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class SuggestionListBuilderTest {
-
-    private SuggestionListBuilder mBuilder;
-    private SuggestionCategory mCategory1;
-    private SuggestionCategory mCategory2;
-    private Suggestion mSuggestion1;
-    private Suggestion mSuggestion2;
-
-    @Before
-    public void setUp() {
-        mSuggestion1 = new Suggestion.Builder("id1")
-                .setTitle("title1")
-                .setSummary("summary1")
-                .build();
-        mCategory1 = SuggestionCategoryRegistry.CATEGORIES.get(3);
-        mCategory2 = SuggestionCategoryRegistry.CATEGORIES.get(4);
-        mSuggestion2 = new Suggestion.Builder("id2")
-                .setTitle("title2")
-                .setSummary("summary2")
-                .build();
-        mBuilder = new SuggestionListBuilder();
-    }
-
-    @Test
-    public void dedupe_shouldSkipSameSuggestion() {
-        mBuilder.addSuggestions(mCategory1, Arrays.asList(mSuggestion1));
-        mBuilder.addSuggestions(mCategory2, Arrays.asList(mSuggestion1));
-
-        assertThat(mBuilder.build()).hasSize(1);
-    }
-
-    @Test
-    public void dedupe_shouldContainDifferentSuggestion() {
-        mBuilder.addSuggestions(mCategory1, Arrays.asList(mSuggestion1, mSuggestion2));
-
-        assertThat(mBuilder.build()).hasSize(2);
-    }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/EventStoreTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/EventStoreTest.java
deleted file mode 100644
index 6680f20..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/EventStoreTest.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * 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.suggestions.ranking;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class EventStoreTest {
-    private EventStore mEventStore;
-
-    @Before
-    public void setUp() {
-        mEventStore = new EventStore(RuntimeEnvironment.application);
-    }
-
-    @Test
-    public void testWriteRead() {
-        mEventStore.writeEvent("pkg", EventStore.EVENT_SHOWN);
-        long timeMs = System.currentTimeMillis();
-        assertThat(mEventStore.readMetric("pkg", EventStore.EVENT_SHOWN, EventStore.METRIC_COUNT))
-                .isEqualTo(1);
-        assertThat(Math.abs(timeMs - mEventStore.readMetric("pkg", EventStore.EVENT_SHOWN,
-                EventStore.METRIC_LAST_EVENT_TIME)) < 10000)
-                .isTrue();
-    }
-
-    @Test
-    public void testWriteRead_shouldHaveLatestValues() {
-        mEventStore.writeEvent("pkg", EventStore.EVENT_DISMISSED);
-        mEventStore.writeEvent("pkg", EventStore.EVENT_DISMISSED);
-        assertThat(
-                mEventStore.readMetric("pkg", EventStore.EVENT_DISMISSED, EventStore.METRIC_COUNT))
-                .isEqualTo(2);
-    }
-
-    @Test
-    public void testWriteRead_shouldReturnDefaultIfNotAvailable() {
-        assertThat(mEventStore.readMetric("pkg", EventStore.EVENT_SHOWN, EventStore.METRIC_COUNT))
-                .isEqualTo(0);
-        assertThat(mEventStore.readMetric("pkg", EventStore.EVENT_SHOWN,
-                EventStore.METRIC_LAST_EVENT_TIME))
-                .isEqualTo(0);
-    }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/SuggestionFeaturizerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/SuggestionFeaturizerTest.java
deleted file mode 100644
index 1f349d3..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/SuggestionFeaturizerTest.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * 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.suggestions.ranking;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.service.settings.suggestions.Suggestion;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class SuggestionFeaturizerTest {
-
-    private EventStore mEventStore;
-    private SuggestionFeaturizer mSuggestionFeaturizer;
-
-    @Before
-    public void setUp() {
-        mEventStore = new EventStore(RuntimeEnvironment.application);
-        mSuggestionFeaturizer = new SuggestionFeaturizer(mEventStore);
-    }
-
-    @Test
-    public void testFeaturize_singlePackage() {
-        mEventStore.writeEvent("pkg", EventStore.EVENT_DISMISSED);
-        mEventStore.writeEvent("pkg", EventStore.EVENT_SHOWN);
-        mEventStore.writeEvent("pkg", EventStore.EVENT_SHOWN);
-        final Map<String, Double> features = mSuggestionFeaturizer
-                .featurize(Arrays.asList(new Suggestion.Builder("pkg").build()))
-                .get("pkg");
-        assertThat(features.get(SuggestionFeaturizer.FEATURE_IS_SHOWN)).isEqualTo(1.0);
-        assertThat(features.get(SuggestionFeaturizer.FEATURE_IS_DISMISSED)).isEqualTo(1.0);
-        assertThat(features.get(SuggestionFeaturizer.FEATURE_IS_CLICKED)).isEqualTo(0.0);
-
-        assertThat(features.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_SHOWN)).isLessThan
-                (1.0);
-        assertThat(features.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_DISMISSED))
-                .isLessThan(1.0);
-        assertThat(features.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_CLICKED))
-                .isEqualTo(1.0);
-        assertThat(features.get(SuggestionFeaturizer.FEATURE_SHOWN_COUNT))
-                .isEqualTo(2.0 / SuggestionFeaturizer.COUNT_NORMALIZATION_FACTOR);
-        assertThat(features.get(SuggestionFeaturizer.FEATURE_DISMISSED_COUNT))
-                .isEqualTo(1.0 / SuggestionFeaturizer.COUNT_NORMALIZATION_FACTOR);
-        assertThat(features.get(SuggestionFeaturizer.FEATURE_CLICKED_COUNT)).isEqualTo(0.0);
-    }
-
-    @Test
-    public void testFeaturize_multiplePackages() {
-        mEventStore.writeEvent("pkg1", EventStore.EVENT_DISMISSED);
-        mEventStore.writeEvent("pkg2", EventStore.EVENT_SHOWN);
-        mEventStore.writeEvent("pkg1", EventStore.EVENT_SHOWN);
-        final List<Suggestion> suggestions = new ArrayList<Suggestion>() {
-            {
-                add(new Suggestion.Builder("pkg1").build());
-                add(new Suggestion.Builder("pkg2").build());
-            }
-        };
-        final Map<String, Map<String, Double>> features = mSuggestionFeaturizer
-                .featurize(suggestions);
-        final Map<String, Double> features1 = features.get("pkg1");
-        final Map<String, Double> features2 = features.get("pkg2");
-
-        assertThat(features1.get(SuggestionFeaturizer.FEATURE_IS_SHOWN)).isEqualTo(1.0);
-        assertThat(features1.get(SuggestionFeaturizer.FEATURE_IS_DISMISSED)).isEqualTo(1.0);
-        assertThat(features1.get(SuggestionFeaturizer.FEATURE_IS_CLICKED)).isEqualTo(0.0);
-        assertThat(features1.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_SHOWN))
-                .isLessThan(1.0);
-        assertThat(features1.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_DISMISSED))
-                .isLessThan(1.0);
-        assertThat(features1.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_CLICKED))
-                .isEqualTo(1.0);
-        assertThat(features1.get(SuggestionFeaturizer.FEATURE_SHOWN_COUNT))
-                .isEqualTo(1.0 / SuggestionFeaturizer.COUNT_NORMALIZATION_FACTOR);
-        assertThat(features1.get(SuggestionFeaturizer.FEATURE_DISMISSED_COUNT))
-                .isEqualTo(1.0 / SuggestionFeaturizer.COUNT_NORMALIZATION_FACTOR);
-        assertThat(features1.get(SuggestionFeaturizer.FEATURE_CLICKED_COUNT)).isEqualTo(0.0);
-
-        assertThat(features2.get(SuggestionFeaturizer.FEATURE_IS_SHOWN)).isEqualTo(1.0);
-        assertThat(features2.get(SuggestionFeaturizer.FEATURE_IS_DISMISSED)).isEqualTo(0.0);
-        assertThat(features2.get(SuggestionFeaturizer.FEATURE_IS_CLICKED)).isEqualTo(0.0);
-        assertThat(features2.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_SHOWN))
-                .isLessThan(1.0);
-        assertThat(features2.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_DISMISSED))
-                .isEqualTo(1.0);
-        assertThat(features2.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_CLICKED))
-                .isEqualTo(1.0);
-        assertThat(features2.get(SuggestionFeaturizer.FEATURE_SHOWN_COUNT))
-                .isEqualTo(1.0 / SuggestionFeaturizer.COUNT_NORMALIZATION_FACTOR);
-        assertThat(features2.get(SuggestionFeaturizer.FEATURE_DISMISSED_COUNT)).isEqualTo(0.0);
-        assertThat(features2.get(SuggestionFeaturizer.FEATURE_CLICKED_COUNT)).isEqualTo(0.0);
-    }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/SuggestionRankerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/SuggestionRankerTest.java
deleted file mode 100644
index f27f719..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/SuggestionRankerTest.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.android.settings.intelligence.suggestions.ranking;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Matchers.same;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.when;
-
-import android.service.settings.suggestions.Suggestion;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class SuggestionRankerTest {
-
-    @Mock
-    private SuggestionRanker mSuggestionRanker;
-    @Mock
-    private SuggestionFeaturizer mSuggestionFeaturizer;
-
-    private Map<String, Map<String, Double>> mFeatures;
-    private List<Suggestion> mSuggestions;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mFeatures = new HashMap<>();
-        mFeatures.put("pkg1", new HashMap<String, Double>());
-        mFeatures.put("pkg2", new HashMap<String, Double>());
-        mFeatures.put("pkg3", new HashMap<String, Double>());
-        mSuggestions = new ArrayList<>();
-
-        mSuggestions.add(new Suggestion.Builder("pkg1").build());
-        mSuggestions.add(new Suggestion.Builder("pkg2").build());
-        mSuggestions.add(new Suggestion.Builder("pkg3").build());
-
-        mSuggestionFeaturizer = mock(SuggestionFeaturizer.class);
-        mSuggestionRanker = new SuggestionRanker(mSuggestionFeaturizer);
-        when(mSuggestionFeaturizer.featurize(mSuggestions)).thenReturn(mFeatures);
-        mSuggestionRanker = spy(mSuggestionRanker);
-        when(mSuggestionRanker.getRelevanceMetric(same(mFeatures.get("pkg1")))).thenReturn(0.9);
-        when(mSuggestionRanker.getRelevanceMetric(same(mFeatures.get("pkg2")))).thenReturn(0.1);
-        when(mSuggestionRanker.getRelevanceMetric(same(mFeatures.get("pkg3")))).thenReturn(0.5);
-    }
-
-    @Test
-    public void testRank() {
-        final List<Suggestion> expectedOrderdList = new ArrayList<>();
-        expectedOrderdList.add(mSuggestions.get(0)); // relevance = 0.9
-        expectedOrderdList.add(mSuggestions.get(2)); // relevance = 0.5
-        expectedOrderdList.add(mSuggestions.get(1)); // relevance = 0.1
-
-        mSuggestionRanker.rankSuggestions(mSuggestions);
-        assertThat(mSuggestions).isEqualTo(expectedOrderdList);
-    }
-}
\ No newline at end of file