Merge "Do not start suggestion loader if host isn't attached."
diff --git a/res/layout/app_preference_item.xml b/res/layout/app_preference_item.xml
index f13ced7..5653920 100755
--- a/res/layout/app_preference_item.xml
+++ b/res/layout/app_preference_item.xml
@@ -27,11 +27,10 @@
     android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
     <FrameLayout
         android:id="@+id/icon_frame"
-        style="@style/preference_icon_frame"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:gravity="start|center_vertical"
-        android:paddingEnd="12dp"
+        android:minWidth="56dp"
         android:paddingTop="4dp"
         android:paddingBottom="4dp">
         <android.support.v7.internal.widget.PreferenceImageView
diff --git a/res/layout/preference_small_icon.xml b/res/layout/preference_small_icon.xml
deleted file mode 100644
index e734b4b..0000000
--- a/res/layout/preference_small_icon.xml
+++ /dev/null
@@ -1,82 +0,0 @@
-<?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.
-  -->
-
-<!-- Same as preference_material except that it uses a 24dp fixed size icon -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-              xmlns:app="http://schemas.android.com/apk/res-auto"
-              android:layout_width="match_parent"
-              android:layout_height="wrap_content"
-              android:minHeight="?android:attr/listPreferredItemHeightSmall"
-              android:gravity="center_vertical"
-              android:paddingStart="?android:attr/listPreferredItemPaddingStart"
-              android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
-              android:background="?android:attr/selectableItemBackground"
-              android:clipToPadding="false"
-              android:focusable="true" >
-
-    <LinearLayout
-        android:id="@+id/icon_frame"
-        style="@style/preference_icon_frame"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:gravity="start|center_vertical"
-        android:orientation="horizontal"
-        android:paddingEnd="12dp"
-        android:paddingTop="4dp"
-        android:paddingBottom="4dp">
-        <android.support.v7.internal.widget.PreferenceImageView
-            android:id="@android:id/icon"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            app:maxWidth="24dp"
-            app:maxHeight="24dp" />
-    </LinearLayout>
-
-    <RelativeLayout
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_weight="1"
-        android:paddingTop="16dp"
-        android:paddingBottom="16dp">
-
-        <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:layout_below="@android:id/title"
-                  android:layout_alignStart="@android:id/title"
-                  android:textAppearance="?android:attr/textAppearanceListItemSecondary"
-                  android:textColor="?android:attr/textColorSecondary"
-                  android:maxLines="10" />
-
-    </RelativeLayout>
-
-    <!-- Preference should place its actual preference widget here. -->
-    <LinearLayout android:id="@android:id/widget_frame"
-                  android:layout_width="wrap_content"
-                  android:layout_height="match_parent"
-                  android:gravity="end|center_vertical"
-                  android:paddingStart="16dp"
-                  android:orientation="vertical" />
-
-</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/storage_item.xml b/res/layout/storage_item.xml
index 4b14bb3..ba1c697 100644
--- a/res/layout/storage_item.xml
+++ b/res/layout/storage_item.xml
@@ -36,12 +36,11 @@
 
         <LinearLayout
             android:id="@+id/icon_frame"
-            style="@style/preference_icon_frame"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:gravity="start|center_vertical"
+            android:minWidth="56dp"
             android:orientation="horizontal"
-            android:paddingEnd="12dp"
             android:paddingTop="4dp"
             android:paddingBottom="4dp">
             <com.android.internal.widget.PreferenceImageView
diff --git a/res/layout/storage_volume.xml b/res/layout/storage_volume.xml
index 39a6f85..ca368b9 100644
--- a/res/layout/storage_volume.xml
+++ b/res/layout/storage_volume.xml
@@ -26,12 +26,11 @@
 
     <LinearLayout
         android:id="@+id/icon_frame"
-        style="@style/preference_icon_frame"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:gravity="start|center_vertical"
+        android:minWidth="56dp"
         android:orientation="horizontal"
-        android:paddingEnd="12dp"
         android:paddingTop="4dp"
         android:paddingBottom="4dp">
         <com.android.internal.widget.PreferenceImageView
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f0721fc..8b84849 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -6471,7 +6471,7 @@
     <string name="dashboard_suggestion_condition_footer_content_description">Collapse</string>
 
     <!-- Title for setting tile leading to network and Internet settings [CHAR LIMIT=40]-->
-    <string name="network_dashboard_title">Network &amp; Internet</string>
+    <string name="network_dashboard_title">Network &amp; internet</string>
     <!-- Summary for Network and Internet settings, explaining it contains mobile network setting [CHAR LIMIT=NONE]-->
     <string name="network_dashboard_summary_mobile">mobile</string>
     <!-- Summary for Network and Internet settings, explaining it contains network data usage setting [CHAR LIMIT=NONE]-->
diff --git a/res/xml/security_settings_misc.xml b/res/xml/security_settings_misc.xml
index ee2c534..6a71033 100644
--- a/res/xml/security_settings_misc.xml
+++ b/res/xml/security_settings_misc.xml
@@ -64,7 +64,7 @@
         android:key="encryption_and_credential"
         android:title="@string/encryption_and_credential_settings_title"
         android:summary="@string/encryption_and_credential_settings_summary"
-        android:fragment="com.android.settings.EncryptionAndCredential"/>
+        android:fragment="com.android.settings.security.EncryptionAndCredential"/>
 
     <Preference android:key="manage_trust_agents"
         android:title="@string/manage_trust_agents"
diff --git a/src/com/android/settings/accounts/AccountPreferenceBase.java b/src/com/android/settings/accounts/AccountPreferenceBase.java
index 7c3b54c..54ddf64 100644
--- a/src/com/android/settings/accounts/AccountPreferenceBase.java
+++ b/src/com/android/settings/accounts/AccountPreferenceBase.java
@@ -23,18 +23,16 @@
 import android.content.SyncStatusObserver;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.support.v7.preference.PreferenceScreen;
 import android.text.format.DateFormat;
 import android.util.Log;
 
 import com.android.settings.SettingsPreferenceFragment;
 import com.android.settings.Utils;
 import com.android.settingslib.accounts.AuthenticatorHelper;
+import com.android.settingslib.utils.ThreadUtils;
 
-import java.util.ArrayList;
 import java.util.Date;
 
 abstract class AccountPreferenceBase extends SettingsPreferenceFragment
@@ -46,8 +44,6 @@
     public static final String AUTHORITIES_FILTER_KEY = "authorities";
     public static final String ACCOUNT_TYPES_FILTER_KEY = "account_types";
 
-    private final Handler mHandler = new Handler();
-
     private UserManager mUm;
     private Object mStatusChangeListenerHandle;
     protected AuthenticatorHelper mAuthenticatorHelper;
@@ -118,29 +114,8 @@
         ContentResolver.removeStatusChangeListener(mStatusChangeListenerHandle);
     }
 
-    private SyncStatusObserver mSyncStatusObserver = new SyncStatusObserver() {
-        public void onStatusChanged(int which) {
-            mHandler.post(new Runnable() {
-                public void run() {
-                    onSyncStateUpdated();
-                }
-            });
-        }
-    };
-
-    public ArrayList<String> getAuthoritiesForAccountType(String type) {
-        return mAuthenticatorHelper.getAuthoritiesForAccountType(type);
-    }
-
-    /**
-     * Gets the preferences.xml file associated with a particular account type.
-     * @param accountType the type of account
-     * @return a PreferenceScreen inflated from accountPreferenceId.
-     */
-    public PreferenceScreen addPreferencesForType(final String accountType,
-            PreferenceScreen parent) {
-        return mAccountTypePreferenceLoader.addPreferencesForType(accountType, parent);
-    }
+    private SyncStatusObserver mSyncStatusObserver =
+            which -> ThreadUtils.postOnMainThread(() -> onSyncStateUpdated());
 
     public void updateAuthDescriptions() {
         mAuthenticatorHelper.updateAuthDescriptions(getActivity());
diff --git a/src/com/android/settings/applications/RunningServiceDetails.java b/src/com/android/settings/applications/RunningServiceDetails.java
index 84c2ee5..7e73a9b 100644
--- a/src/com/android/settings/applications/RunningServiceDetails.java
+++ b/src/com/android/settings/applications/RunningServiceDetails.java
@@ -21,7 +21,6 @@
 import android.content.res.Resources;
 import android.os.Bundle;
 import android.os.Debug;
-import android.os.Handler;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -37,6 +36,7 @@
 import com.android.settings.Utils;
 import com.android.settings.core.InstrumentedPreferenceFragment;
 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+import com.android.settingslib.utils.ThreadUtils;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -481,15 +481,12 @@
             addDetailViews();
         }
     }
-    
+
     private void finish() {
-        (new Handler()).post(new Runnable() {
-            @Override
-            public void run() {
-                Activity a = getActivity();
-                if (a != null) {
-                    a.onBackPressed();
-                }
+        ThreadUtils.postOnMainThread(() -> {
+            final Activity a = getActivity();
+            if (a != null) {
+                a.onBackPressed();
             }
         });
     }
diff --git a/src/com/android/settings/applications/defaultapps/DefaultAutofillPicker.java b/src/com/android/settings/applications/defaultapps/DefaultAutofillPicker.java
index 00dedf9..97f6d4b 100644
--- a/src/com/android/settings/applications/defaultapps/DefaultAutofillPicker.java
+++ b/src/com/android/settings/applications/defaultapps/DefaultAutofillPicker.java
@@ -27,7 +27,6 @@
 import android.content.pm.ServiceInfo;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.Handler;
 import android.provider.Settings;
 import android.service.autofill.AutofillService;
 import android.service.autofill.AutofillServiceInfo;
@@ -39,6 +38,7 @@
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.logging.nano.MetricsProto;
 import com.android.settings.R;
+import com.android.settingslib.utils.ThreadUtils;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -59,7 +59,6 @@
      * Set when the fragment is implementing ACTION_REQUEST_SET_AUTOFILL_SERVICE.
      */
     private DialogInterface.OnClickListener mCancelListener;
-    private final Handler mHandler = new Handler();
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -123,17 +122,17 @@
     private final PackageMonitor mSettingsPackageMonitor = new PackageMonitor() {
         @Override
         public void onPackageAdded(String packageName, int uid) {
-            mHandler.post(() -> update());
+            ThreadUtils.postOnMainThread(() -> update());
         }
 
         @Override
         public void onPackageModified(String packageName) {
-            mHandler.post(() -> update());
+            ThreadUtils.postOnMainThread(() -> update());
         }
 
         @Override
         public void onPackageRemoved(String packageName, int uid) {
-            mHandler.post(() -> update());
+            ThreadUtils.postOnMainThread(() -> update());
         }
     };
 
diff --git a/src/com/android/settings/core/InstrumentedPreferenceFragment.java b/src/com/android/settings/core/InstrumentedPreferenceFragment.java
index 8ef8c0e..5b95d66 100644
--- a/src/com/android/settings/core/InstrumentedPreferenceFragment.java
+++ b/src/com/android/settings/core/InstrumentedPreferenceFragment.java
@@ -85,7 +85,7 @@
     }
 
     public static boolean usePreferenceScreenTitle() {
-        return FeatureFlagUtils.isEnabled(FEATURE_FLAG_USE_PREFERENCE_SCREEN_TITLE);
+        return FeatureFlagUtils.isEnabled(FEATURE_FLAG_USE_PREFERENCE_SCREEN_TITLE) || true;
     }
 
     protected final Context getPrefContext() {
diff --git a/src/com/android/settings/dashboard/DashboardFragment.java b/src/com/android/settings/dashboard/DashboardFragment.java
index e7f36e1..6796c26 100644
--- a/src/com/android/settings/dashboard/DashboardFragment.java
+++ b/src/com/android/settings/dashboard/DashboardFragment.java
@@ -183,6 +183,9 @@
         }
     }
 
+    @Override
+    protected abstract int getPreferenceScreenResId();
+
     protected <T extends AbstractPreferenceController> T getPreferenceController(Class<T> clazz) {
         AbstractPreferenceController controller = mPreferenceControllers.get(clazz);
         return (T) controller;
diff --git a/src/com/android/settings/dashboard/SummaryLoader.java b/src/com/android/settings/dashboard/SummaryLoader.java
index 4586a55..5816bba8 100644
--- a/src/com/android/settings/dashboard/SummaryLoader.java
+++ b/src/com/android/settings/dashboard/SummaryLoader.java
@@ -35,6 +35,7 @@
 import com.android.settings.overlay.FeatureFactory;
 import com.android.settingslib.drawer.DashboardCategory;
 import com.android.settingslib.drawer.Tile;
+import com.android.settingslib.utils.ThreadUtils;
 
 import java.lang.reflect.Field;
 import java.util.List;
@@ -52,7 +53,6 @@
     private final String mCategoryKey;
 
     private final Worker mWorker;
-    private final Handler mHandler;
     private final HandlerThread mWorkerThread;
 
     private SummaryConsumer mSummaryConsumer;
@@ -64,7 +64,6 @@
         mDashboardFeatureProvider = FeatureFactory.getFactory(activity)
                 .getDashboardFeatureProvider(activity);
         mCategoryKey = null;
-        mHandler = new Handler();
         mWorkerThread = new HandlerThread("SummaryLoader", Process.THREAD_PRIORITY_BACKGROUND);
         mWorkerThread.start();
         mWorker = new Worker(mWorkerThread.getLooper());
@@ -82,7 +81,6 @@
         mDashboardFeatureProvider = FeatureFactory.getFactory(activity)
                 .getDashboardFeatureProvider(activity);
         mCategoryKey = categoryKey;
-        mHandler = new Handler();
         mWorkerThread = new HandlerThread("SummaryLoader", Process.THREAD_PRIORITY_BACKGROUND);
         mWorkerThread.start();
         mWorker = new Worker(mWorkerThread.getLooper());
@@ -112,25 +110,22 @@
 
     public void setSummary(SummaryProvider provider, final CharSequence summary) {
         final ComponentName component = mSummaryProviderMap.get(provider);
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
+        ThreadUtils.postOnMainThread(() -> {
 
-                final Tile tile = getTileFromCategory(
-                        mDashboardFeatureProvider.getTilesForCategory(mCategoryKey), component);
+            final Tile tile = getTileFromCategory(
+                    mDashboardFeatureProvider.getTilesForCategory(mCategoryKey), component);
 
-                if (tile == null) {
-                    if (DEBUG) {
-                        Log.d(TAG, "Can't find tile for " + component);
-                    }
-                    return;
-                }
+            if (tile == null) {
                 if (DEBUG) {
-                    Log.d(TAG, "setSummary " + tile.title + " - " + summary);
+                    Log.d(TAG, "Can't find tile for " + component);
                 }
-
-                updateSummaryIfNeeded(tile, summary);
+                return;
             }
+            if (DEBUG) {
+                Log.d(TAG, "setSummary " + tile.title + " - " + summary);
+            }
+
+            updateSummaryIfNeeded(tile, summary);
         });
     }
 
diff --git a/src/com/android/settings/dashboard/SupportFragment.java b/src/com/android/settings/dashboard/SupportFragment.java
index 8a1a79b..fcc9f78 100644
--- a/src/com/android/settings/dashboard/SupportFragment.java
+++ b/src/com/android/settings/dashboard/SupportFragment.java
@@ -27,8 +27,6 @@
 import android.net.NetworkInfo;
 import android.net.NetworkRequest;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
 import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
 import android.view.LayoutInflater;
@@ -40,6 +38,7 @@
 import com.android.settings.core.InstrumentedFragment;
 import com.android.settings.overlay.FeatureFactory;
 import com.android.settings.overlay.SupportFeatureProvider;
+import com.android.settingslib.utils.ThreadUtils;
 
 /**
  * Fragment for support tab in SettingsGoogle.
@@ -47,7 +46,6 @@
 public final class SupportFragment extends InstrumentedFragment implements View.OnClickListener,
         OnAccountsUpdateListener {
 
-    private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final ConnectivityManager.NetworkCallback mNetworkCallback =
             new ConnectivityManager.NetworkCallback() {
 
@@ -152,12 +150,9 @@
     }
 
     private void postConnectivityChanged() {
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                if (mSupportItemAdapter != null) {
-                    mSupportItemAdapter.setHasInternet(hasInternet());
-                }
+        ThreadUtils.postOnMainThread(() -> {
+            if (mSupportItemAdapter != null) {
+                mSupportItemAdapter.setHasInternet(hasInternet());
             }
         });
     }
diff --git a/src/com/android/settings/datausage/DataSaverBackend.java b/src/com/android/settings/datausage/DataSaverBackend.java
index f37a3c3..041a81f 100644
--- a/src/com/android/settings/datausage/DataSaverBackend.java
+++ b/src/com/android/settings/datausage/DataSaverBackend.java
@@ -14,24 +14,23 @@
 
 package com.android.settings.datausage;
 
+import static android.net.NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND;
+import static android.net.NetworkPolicyManager.POLICY_NONE;
+import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND;
+
 import android.content.Context;
 import android.net.INetworkPolicyListener;
 import android.net.NetworkPolicyManager;
-import android.os.Handler;
-import android.os.Looper;
 import android.os.RemoteException;
 import android.util.SparseIntArray;
 
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.settings.core.instrumentation.MetricsFeatureProvider;
 import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.utils.ThreadUtils;
 
 import java.util.ArrayList;
 
-import static android.net.NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND;
-import static android.net.NetworkPolicyManager.POLICY_NONE;
-import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND;
-
 public class DataSaverBackend {
 
     private static final String TAG = "DataSaverBackend";
@@ -39,7 +38,6 @@
     private final Context mContext;
     private final MetricsFeatureProvider mMetricsFeatureProvider;
 
-    private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final NetworkPolicyManager mPolicyManager;
     private final ArrayList<Listener> mListeners = new ArrayList<>();
     private SparseIntArray mUidPolicies = new SparseIntArray();
@@ -195,7 +193,7 @@
 
         @Override
         public void onUidPoliciesChanged(final int uid, final int uidPolicies) {
-            mHandler.post(() -> handleUidPoliciesChanged(uid, uidPolicies));
+            ThreadUtils.postOnMainThread(() -> handleUidPoliciesChanged(uid, uidPolicies));
         }
 
         @Override
@@ -204,7 +202,7 @@
 
         @Override
         public void onRestrictBackgroundChanged(final boolean isDataSaving) throws RemoteException {
-            mHandler.post(() -> handleRestrictBackgroundChanged(isDataSaving));
+            ThreadUtils.postOnMainThread(() -> handleRestrictBackgroundChanged(isDataSaving));
         }
     };
 
diff --git a/src/com/android/settings/display/ShowOperatorNamePreferenceController.java b/src/com/android/settings/display/ShowOperatorNamePreferenceController.java
index 8341519..4573c6d 100644
--- a/src/com/android/settings/display/ShowOperatorNamePreferenceController.java
+++ b/src/com/android/settings/display/ShowOperatorNamePreferenceController.java
@@ -19,10 +19,11 @@
 import android.support.v7.preference.Preference;
 
 import com.android.settings.R;
+import com.android.settings.core.PreferenceControllerMixin;
 import com.android.settingslib.core.AbstractPreferenceController;
 
 public class ShowOperatorNamePreferenceController extends AbstractPreferenceController
-        implements Preference.OnPreferenceChangeListener {
+        implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener {
 
     private static final String KEY_SHOW_OPERATOR_NAME = "show_operator_name";
 
diff --git a/src/com/android/settings/enterprise/EnterpriseSetDefaultAppsListPreferenceController.java b/src/com/android/settings/enterprise/EnterpriseSetDefaultAppsListPreferenceController.java
index 03a78ea..0927299 100644
--- a/src/com/android/settings/enterprise/EnterpriseSetDefaultAppsListPreferenceController.java
+++ b/src/com/android/settings/enterprise/EnterpriseSetDefaultAppsListPreferenceController.java
@@ -20,7 +20,6 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
-import android.os.Handler;
 import android.os.UserHandle;
 import android.support.v7.preference.Preference;
 import android.support.v7.preference.PreferenceCategory;
@@ -36,6 +35,7 @@
 import com.android.settings.overlay.FeatureFactory;
 import com.android.settings.users.UserFeatureProvider;
 import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settingslib.utils.ThreadUtils;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -100,7 +100,7 @@
                 userMap.put(typeOfDefault, applicationInfos);
             }
         }
-        new Handler(mContext.getMainLooper()).post(() -> { updateUi(); });
+        ThreadUtils.postOnMainThread(() -> updateUi());
     }
 
     @Override
diff --git a/src/com/android/settings/fingerprint/FingerprintEnrollEnrolling.java b/src/com/android/settings/fingerprint/FingerprintEnrollEnrolling.java
index 10bc9e3..756f826 100644
--- a/src/com/android/settings/fingerprint/FingerprintEnrollEnrolling.java
+++ b/src/com/android/settings/fingerprint/FingerprintEnrollEnrolling.java
@@ -32,6 +32,8 @@
 import android.hardware.fingerprint.FingerprintManager;
 import android.os.Bundle;
 import android.os.UserHandle;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
 import android.text.TextUtils;
 import android.view.MotionEvent;
 import android.view.View;
@@ -74,6 +76,9 @@
      */
     private static final int ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3;
 
+    private static final VibrationEffect VIBRATE_EFFECT_ERROR =
+            VibrationEffect.createWaveform(new long[] {0, 5, 55, 60}, -1);
+
     private ProgressBar mProgressBar;
     private ObjectAnimator mProgressAnim;
     private TextView mStartMessage;
@@ -90,6 +95,7 @@
     private int mIndicatorBackgroundRestingColor;
     private int mIndicatorBackgroundActivatedColor;
     private boolean mRestoring;
+    private Vibrator mVibrator;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -100,6 +106,7 @@
         mRepeatMessage = (TextView) findViewById(R.id.repeat_message);
         mErrorText = (TextView) findViewById(R.id.error_text);
         mProgressBar = (ProgressBar) findViewById(R.id.fingerprint_progress_bar);
+        mVibrator = getSystemService(Vibrator.class);
 
         Button skipButton = findViewById(R.id.skip_button);
         skipButton.setOnClickListener(this);
@@ -368,6 +375,9 @@
             mErrorText.setAlpha(1f);
             mErrorText.setTranslationY(0f);
         }
+        if (isResumed()) {
+            mVibrator.vibrate(VIBRATE_EFFECT_ERROR);
+        }
     }
 
     private void clearError() {
@@ -378,12 +388,7 @@
                             R.dimen.fingerprint_error_text_disappear_distance))
                     .setDuration(100)
                     .setInterpolator(mFastOutLinearInInterpolator)
-                    .withEndAction(new Runnable() {
-                        @Override
-                        public void run() {
-                            mErrorText.setVisibility(View.INVISIBLE);
-                        }
-                    })
+                    .withEndAction(() -> mErrorText.setVisibility(View.INVISIBLE))
                     .start();
         }
     }
diff --git a/src/com/android/settings/network/VpnPreferenceController.java b/src/com/android/settings/network/VpnPreferenceController.java
index 763fb72..95513e0 100644
--- a/src/com/android/settings/network/VpnPreferenceController.java
+++ b/src/com/android/settings/network/VpnPreferenceController.java
@@ -23,8 +23,6 @@
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
-import android.os.Handler;
-import android.os.Looper;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.UserHandle;
@@ -45,6 +43,7 @@
 import com.android.settingslib.core.lifecycle.LifecycleObserver;
 import com.android.settingslib.core.lifecycle.events.OnPause;
 import com.android.settingslib.core.lifecycle.events.OnResume;
+import com.android.settingslib.utils.ThreadUtils;
 
 import java.util.List;
 
@@ -157,7 +156,7 @@
         } else {
             summary = getNameForVpnConfig(vpn, UserHandle.of(uid));
         }
-        new Handler(Looper.getMainLooper()).post(() -> mPreference.setSummary(summary));
+        ThreadUtils.postOnMainThread(() -> mPreference.setSummary(summary));
     }
 
     private String getNameForVpnConfig(VpnConfig cfg, UserHandle user) {
diff --git a/src/com/android/settings/search/AccessibilityServiceResultLoader.java b/src/com/android/settings/search/AccessibilityServiceResultLoader.java
index 345ab30..327aef2 100644
--- a/src/com/android/settings/search/AccessibilityServiceResultLoader.java
+++ b/src/com/android/settings/search/AccessibilityServiceResultLoader.java
@@ -30,105 +30,112 @@
 import android.support.annotation.VisibleForTesting;
 import android.support.v4.content.ContextCompat;
 import android.util.IconDrawableFactory;
+import android.util.Log;
 import android.view.accessibility.AccessibilityManager;
 
 import com.android.settings.R;
 import com.android.settings.accessibility.AccessibilitySettings;
 import com.android.settings.dashboard.SiteMapManager;
-import com.android.settings.utils.AsyncLoader;
 
-import java.util.HashSet;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
 
-public class AccessibilityServiceResultLoader extends AsyncLoader<Set<? extends SearchResult>> {
+public class AccessibilityServiceResultLoader extends
+        FutureTask<List<? extends SearchResult>> {
 
-    private static final int NAME_NO_MATCH = -1;
-
-    private final Context mContext;
-
-    private List<String> mBreadcrumb;
-    private SiteMapManager mSiteMapManager;
-    @VisibleForTesting
-    final String mQuery;
-    private final AccessibilityManager mAccessibilityManager;
-    private final PackageManager mPackageManager;
-    private final int mUserId;
-
+    private static final String TAG = "A11yResultFutureTask";
 
     public AccessibilityServiceResultLoader(Context context, String query,
-            SiteMapManager mapManager) {
-        super(context);
-        mContext = context;
-        mUserId = UserHandle.myUserId();
-        mSiteMapManager = mapManager;
-        mPackageManager = context.getPackageManager();
-        mAccessibilityManager =
-                (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
-        mQuery = query;
+            SiteMapManager manager) {
+        super(new AccessibilityServiceResultCallable(context, query, manager));
     }
 
-    @Override
-    public Set<? extends SearchResult> loadInBackground() {
-        final Set<SearchResult> results = new HashSet<>();
-        final Context context = getContext();
-        final List<AccessibilityServiceInfo> services = mAccessibilityManager
-                .getInstalledAccessibilityServiceList();
-        final IconDrawableFactory iconFactory = IconDrawableFactory.newInstance(mContext);
-        final String screenTitle = context.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 = getWordDifference(title.toString(), mQuery);
-            if (wordDiff == NAME_NO_MATCH) {
-                continue;
-            }
-            final Drawable icon;
-            if (resolveInfo.getIconResource() == 0) {
-                icon = ContextCompat.getDrawable(context, R.mipmap.ic_accessibility_generic);
-            } else {
-                icon = iconFactory.getBadgedIcon(
-                        resolveInfo.serviceInfo,
-                        resolveInfo.serviceInfo.applicationInfo,
-                        mUserId);
-            }
-            final String componentName = new ComponentName(serviceInfo.packageName,
-                    serviceInfo.name).flattenToString();
-            final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(context,
-                    AccessibilitySettings.class.getName(), componentName, screenTitle);
+    static class AccessibilityServiceResultCallable implements
+            Callable<List<? extends SearchResult>> {
 
-            results.add(new SearchResult.Builder()
-                    .setTitle(title)
-                    .addBreadcrumbs(getBreadCrumb())
-                    .setPayload(new ResultPayload(intent))
-                    .setRank(wordDiff)
-                    .setIcon(icon)
-                    .setStableId(Objects.hash(screenTitle, componentName))
-                    .build());
+        private static final int NAME_NO_MATCH = -1;
+
+        private final Context mContext;
+        private List<String> mBreadcrumb;
+        private SiteMapManager mSiteMapManager;
+        @VisibleForTesting
+        final String mQuery;
+        private final AccessibilityManager mAccessibilityManager;
+        private final PackageManager mPackageManager;
+        private final int mUserId;
+
+        public AccessibilityServiceResultCallable(Context context, String query,
+                SiteMapManager mapManager) {
+            mUserId = UserHandle.myUserId();
+            mContext = context;
+            mSiteMapManager = mapManager;
+            mPackageManager = context.getPackageManager();
+            mAccessibilityManager =
+                    (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+            mQuery = query;
         }
-        return results;
-    }
 
-    private List<String> getBreadCrumb() {
-        if (mBreadcrumb == null || mBreadcrumb.isEmpty()) {
-            final Context context = getContext();
-            mBreadcrumb = mSiteMapManager.buildBreadCrumb(
-                    context, AccessibilitySettings.class.getName(),
-                    context.getString(R.string.accessibility_settings));
+        @Override
+        public List<? extends SearchResult> call() throws Exception {
+            long startTime = System.currentTimeMillis();
+            final List<SearchResult> results = new ArrayList<>();
+            final List<AccessibilityServiceInfo> services = mAccessibilityManager
+                    .getInstalledAccessibilityServiceList();
+            final IconDrawableFactory iconFactory = IconDrawableFactory.newInstance(mContext);
+            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 = getWordDifference(title.toString(), mQuery);
+                if (wordDiff == NAME_NO_MATCH) {
+                    continue;
+                }
+                final Drawable icon;
+                if (resolveInfo.getIconResource() == 0) {
+                    icon = ContextCompat.getDrawable(mContext, R.mipmap.ic_accessibility_generic);
+                } else {
+                    icon = iconFactory.getBadgedIcon(
+                            resolveInfo.serviceInfo,
+                            resolveInfo.serviceInfo.applicationInfo,
+                            mUserId);
+                }
+                final String componentName = new ComponentName(serviceInfo.packageName,
+                        serviceInfo.name).flattenToString();
+                final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(mContext,
+                        AccessibilitySettings.class.getName(), componentName, screenTitle);
+
+                results.add(new SearchResult.Builder()
+                        .setTitle(title)
+                        .addBreadcrumbs(getBreadCrumb())
+                        .setPayload(new ResultPayload(intent))
+                        .setRank(wordDiff)
+                        .setIcon(icon)
+                        .setStableId(Objects.hash(screenTitle, componentName))
+                        .build());
+            }
+            Collections.sort(results);
+            Log.i(TAG, "A11y search loading took:" + (System.currentTimeMillis() - startTime));
+            return results;
         }
-        return mBreadcrumb;
-    }
 
-    @Override
-    protected void onDiscardResult(Set<? extends SearchResult> result) {
-
+        private List<String> getBreadCrumb() {
+            if (mBreadcrumb == null || mBreadcrumb.isEmpty()) {
+                mBreadcrumb = mSiteMapManager.buildBreadCrumb(
+                        mContext, AccessibilitySettings.class.getName(),
+                        mContext.getString(R.string.accessibility_settings));
+            }
+            return mBreadcrumb;
+        }
     }
 }
diff --git a/src/com/android/settings/search/CursorToSearchResultConverter.java b/src/com/android/settings/search/CursorToSearchResultConverter.java
index ce64de9..8528c56 100644
--- a/src/com/android/settings/search/CursorToSearchResultConverter.java
+++ b/src/com/android/settings/search/CursorToSearchResultConverter.java
@@ -36,16 +36,6 @@
 import java.util.Set;
 
 import static com.android.settings.search.DatabaseResultLoader.BASE_RANKS;
-import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_CLASS_NAME;
-import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_ICON;
-import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_ID;
-import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE;
-import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_KEY;
-import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_PAYLOAD;
-import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_PAYLOAD_TYPE;
-import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_SCREEN_TITLE;
-import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_SUMMARY_ON;
-import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_TITLE;
 import static com.android.settings.search.SearchResult.TOP_RANK;
 
 /**
@@ -62,6 +52,25 @@
 
     private static final String TAG = "CursorConverter";
 
+    /**
+     * These indices are used to match the columns of the this loader's SELECT statement.
+     * These are not necessarily the same order nor similar coverage as the schema defined in
+     * IndexDatabaseHelper
+     */
+    public static final int COLUMN_INDEX_ID = 0;
+    public static final int COLUMN_INDEX_TITLE = 1;
+    public static final int COLUMN_INDEX_SUMMARY_ON = 2;
+    public static final int COLUMN_INDEX_SUMMARY_OFF = 3;
+    public static final int COLUMN_INDEX_CLASS_NAME = 4;
+    public static final int COLUMN_INDEX_SCREEN_TITLE = 5;
+    public static final int COLUMN_INDEX_ICON = 6;
+    public static final int COLUMN_INDEX_INTENT_ACTION = 7;
+    public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE = 8;
+    public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS = 9;
+    public static final int COLUMN_INDEX_KEY = 10;
+    public static final int COLUMN_INDEX_PAYLOAD_TYPE = 11;
+    public static final int COLUMN_INDEX_PAYLOAD = 12;
+
     private final Context mContext;
 
     private final int LONG_TITLE_LENGTH = 20;
diff --git a/src/com/android/settings/search/DatabaseIndexingManager.java b/src/com/android/settings/search/DatabaseIndexingManager.java
index e94befb..970b50f 100644
--- a/src/com/android/settings/search/DatabaseIndexingManager.java
+++ b/src/com/android/settings/search/DatabaseIndexingManager.java
@@ -17,11 +17,13 @@
 
 package com.android.settings.search;
 
-import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_ID;
-import static com.android.settings.search.DatabaseResultLoader
+
+import static com.android.settings.search.CursorToSearchResultConverter.COLUMN_INDEX_ID;
+import static com.android.settings.search.CursorToSearchResultConverter
         .COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE;
-import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_KEY;
+import static com.android.settings.search.CursorToSearchResultConverter.COLUMN_INDEX_KEY;
 import static com.android.settings.search.DatabaseResultLoader.SELECT_COLUMNS;
+import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DOCID;
 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.CLASS_NAME;
 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_ENTRIES;
 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS;
@@ -31,7 +33,6 @@
         .DATA_SUMMARY_ON_NORMALIZED;
 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE;
 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED;
-import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DOCID;
 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ENABLED;
 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ICON;
 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_ACTION;
diff --git a/src/com/android/settings/search/DatabaseResultLoader.java b/src/com/android/settings/search/DatabaseResultLoader.java
index c1663ab..66548a4 100644
--- a/src/com/android/settings/search/DatabaseResultLoader.java
+++ b/src/com/android/settings/search/DatabaseResultLoader.java
@@ -24,35 +24,31 @@
 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.dashboard.SiteMapManager;
-import com.android.settings.utils.AsyncLoader;
+import com.android.settings.overlay.FeatureFactory;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.Callable;
+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.
+ * AsyncTask to retrieve Settings, first party app and any intent based results.
  */
-public class DatabaseResultLoader extends AsyncLoader<Set<? extends SearchResult>> {
-    private static final String LOG = "DatabaseResultLoader";
+public class DatabaseResultLoader extends FutureTask<List<? extends SearchResult>> {
 
-    /* These indices are used to match the columns of the this loader's SELECT statement.
-     These are not necessarily the same order nor similar coverage as the schema defined in
-     IndexDatabaseHelper */
-    public static final int COLUMN_INDEX_ID = 0;
-    public static final int COLUMN_INDEX_TITLE = 1;
-    public static final int COLUMN_INDEX_SUMMARY_ON = 2;
-    public static final int COLUMN_INDEX_SUMMARY_OFF = 3;
-    public static final int COLUMN_INDEX_CLASS_NAME = 4;
-    public static final int COLUMN_INDEX_SCREEN_TITLE = 5;
-    public static final int COLUMN_INDEX_ICON = 6;
-    public static final int COLUMN_INDEX_INTENT_ACTION = 7;
-    public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE = 8;
-    public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS = 9;
-    public static final int COLUMN_INDEX_KEY = 10;
-    public static final int COLUMN_INDEX_PAYLOAD_TYPE = 11;
-    public static final int COLUMN_INDEX_PAYLOAD = 12;
+    private static final String TAG = "DatabaseResultLoader";
 
     public static final String[] SELECT_COLUMNS = {
             IndexColumns.DOCID,
@@ -82,194 +78,267 @@
             IndexColumns.DATA_SUMMARY_OFF_NORMALIZED,
     };
 
-    public static final String[] MATCH_COLUMNS_TERTIARY = {
-            IndexColumns.DATA_KEYWORDS,
-            IndexColumns.DATA_ENTRIES
-    };
-
     /**
      * 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 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
      */
     public static final int[] BASE_RANKS = {1, 3, 7, 9};
 
-    @VisibleForTesting
-    final String mQueryText;
-    private final Context mContext;
-    private final CursorToSearchResultConverter mConverter;
-    private final SiteMapManager mSiteMapManager;
-
-    public DatabaseResultLoader(Context context, String queryText, SiteMapManager mapManager) {
-        super(context);
-        mSiteMapManager = mapManager;
-        mContext = context;
-        mQueryText = queryText;
-        mConverter = new CursorToSearchResultConverter(context);
+    public DatabaseResultLoader(Context context, String query, SiteMapManager manager) {
+        super(new StaticSearchResultCallable(context, query, manager));
     }
 
-    @Override
-    protected void onDiscardResult(Set<? extends SearchResult> result) {
-        // TODO Search
-    }
+    static class StaticSearchResultCallable implements
+            Callable<List<? extends SearchResult>> {
 
-    @Override
-    public Set<? extends SearchResult> loadInBackground() {
-        if (mQueryText == null || mQueryText.isEmpty()) {
-            return null;
+        public final String[] MATCH_COLUMNS_TERTIARY = {
+                IndexColumns.DATA_KEYWORDS,
+                IndexColumns.DATA_ENTRIES
+        };
+
+        @VisibleForTesting
+        final String mQueryText;
+        private final Context mContext;
+        private final CursorToSearchResultConverter mConverter;
+        private final SiteMapManager mSiteMapManager;
+        private final SearchFeatureProvider mFeatureProvider;
+
+        public StaticSearchResultCallable(Context context, String queryText,
+                SiteMapManager mapManager) {
+            mContext = context;
+            mSiteMapManager = mapManager;
+            mQueryText = queryText;
+            mConverter = new CursorToSearchResultConverter(context);
+            mFeatureProvider = FeatureFactory.getFactory(context).getSearchFeatureProvider();
         }
 
-        final Set<SearchResult> results = new HashSet<>();
+        @Override
+        public List<? extends SearchResult> call() {
+            if (mQueryText == null || mQueryText.isEmpty()) {
+                return new ArrayList<>();
+            }
 
-        results.addAll(firstWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[0]));
-        results.addAll(secondaryWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[1]));
-        results.addAll(anyWordQuery(MATCH_COLUMNS_SECONDARY, BASE_RANKS[2]));
-        results.addAll(anyWordQuery(MATCH_COLUMNS_TERTIARY, BASE_RANKS[3]));
-        return results;
-    }
+            // TODO (b/68656233) Consolidate timing metrics
+            long startTime = System.currentTimeMillis();
+            // Start a Future to get search result scores.
+            FutureTask<List<Pair<String, Float>>> rankerTask = mFeatureProvider.getRankerTask(
+                    mContext, mQueryText);
 
-    @Override
-    protected boolean onCancelLoad() {
-        // TODO
-        return super.onCancelLoad();
-    }
+            if (rankerTask != null) {
+                ExecutorService executorService = mFeatureProvider.getExecutorService();
+                executorService.execute(rankerTask);
+            }
 
-    /**
-     * 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 = mQueryText + "%";
-        final String[] selection = buildSingleWordSelection(query, matchColumns.length);
+            final Set<SearchResult> resultSet = new HashSet<>();
 
-        return query(whereClause, selection, baseRank);
-    }
+            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]));
 
-    /**
-     * 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 = "% " + mQueryText + "%";
-        final String[] selection = buildSingleWordSelection(query, matchColumns.length);
+            // 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);
+                }
+            }
 
-        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(mSiteMapManager, resultCursor, baseRank);
+            List<SearchResult> resultList = new ArrayList<>(resultSet);
+            Collections.sort(resultList);
+            Log.i(TAG, "Static search loading took:" + (System.currentTimeMillis() - startTime));
+            return resultList;
         }
-    }
 
-    /**
-     * 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 ");
+        // 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 = mQueryText + "%";
+            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 = "% " + mQueryText + "%";
+            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(mSiteMapManager, resultCursor, baseRank);
             }
         }
-        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 ");
+        /**
+         * 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();
         }
-        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;
+        /**
+         * 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();
         }
-        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 = mQueryText + "%";
-        final String subStringQuery = "% " + mQueryText + "%";
+        /**
+         * 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 - 1); i += 2) {
-            selection[i] = query;
-            selection[i + 1] = subStringQuery;
+            for (int i = 0; i < size; i++) {
+                selection[i] = query;
+            }
+            return selection;
         }
-        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 = mQueryText + "%";
+            final String subStringQuery = "% " + mQueryText + "%";
+
+            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,
+                List<Pair<String, Float>> searchRankScores) {
+            TreeSet<SearchResult> dbResultsSortedByScores = new TreeSet<>(
+                    (o1, o2) -> {
+                        float score1 = getRankingScoreByStableId(searchRankScores, o1.stableId);
+                        float score2 = getRankingScoreByStableId(searchRankScores, o2.stableId);
+                        if (score1 > score2) {
+                            return -1;
+                        } else if (score1 == score2) {
+                            return 0;
+                        } else {
+                            return 1;
+                        }
+                    });
+            dbResultsSortedByScores.addAll(unsortedSet);
+
+            return new ArrayList<>(dbResultsSortedByScores);
+        }
+
+        /**
+         * Looks up ranking score for stableId
+         *
+         * @param stableId String of stableId
+         * @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 getRankingScoreByStableId(List<Pair<String, Float>> searchRankScores, int stableId) {
+            for (Pair<String, Float> rankingScore : searchRankScores) {
+                if (Integer.toString(stableId).compareTo(rankingScore.first) == 0) {
+                    return rankingScore.second;
+                }
+            }
+            // If stableId not found in the list, we assign the minimum score so it will appear at
+            // the end of the list.
+            Log.w(TAG, "stableId " + stableId + " was not in the ranking scores.");
+            return -Float.MAX_VALUE;
+        }
     }
 }
\ No newline at end of file
diff --git a/src/com/android/settings/search/InputDeviceResultLoader.java b/src/com/android/settings/search/InputDeviceResultLoader.java
index e5e6553..598281c 100644
--- a/src/com/android/settings/search/InputDeviceResultLoader.java
+++ b/src/com/android/settings/search/InputDeviceResultLoader.java
@@ -26,6 +26,7 @@
 import android.hardware.input.InputManager;
 import android.hardware.input.KeyboardLayout;
 import android.support.annotation.VisibleForTesting;
+import android.util.Log;
 import android.view.InputDevice;
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodManager;
@@ -35,20 +36,24 @@
 import com.android.settings.dashboard.SiteMapManager;
 import com.android.settings.inputmethod.AvailableVirtualKeyboardFragment;
 import com.android.settings.inputmethod.PhysicalKeyboardFragment;
-import com.android.settings.utils.AsyncLoader;
 import com.android.settingslib.inputmethod.InputMethodAndSubtypeUtil;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
 
 /**
  * Search result for input devices (physical/virtual keyboard, game controllers, etc)
  */
-public class InputDeviceResultLoader extends AsyncLoader<Set<? extends SearchResult>> {
-    private static final int NAME_NO_MATCH = -1;
+
+public class InputDeviceResultLoader extends FutureTask<List<? extends SearchResult>> {
+
+    private static final String TAG = "InputResultFutureTask";
 
     @VisibleForTesting
     static final String PHYSICAL_KEYBOARD_FRAGMENT = PhysicalKeyboardFragment.class.getName();
@@ -56,145 +61,151 @@
     static final String VIRTUAL_KEYBOARD_FRAGMENT =
             AvailableVirtualKeyboardFragment.class.getName();
 
-    private final SiteMapManager mSiteMapManager;
-    private final InputManager mInputManager;
-    private final InputMethodManager mImm;
-    private final PackageManager mPackageManager;
-    @VisibleForTesting
-    final String mQuery;
-
-    private List<String> mPhysicalKeyboardBreadcrumb;
-    private List<String> mVirtualKeyboardBreadcrumb;
-
-    public InputDeviceResultLoader(Context context, String query, SiteMapManager mapManager) {
-        super(context);
-        mQuery = query;
-        mSiteMapManager = mapManager;
-        mInputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
-        mImm = (InputMethodManager) context.getSystemService(INPUT_METHOD_SERVICE);
-        mPackageManager = context.getPackageManager();
+    public InputDeviceResultLoader(Context context, String query, SiteMapManager manager) {
+        super(new InputDeviceResultCallable(context, query, manager));
     }
 
-    @Override
-    protected void onDiscardResult(Set<? extends SearchResult> result) {
-    }
+    static class InputDeviceResultCallable implements
+            Callable<List<? extends SearchResult>> {
+        private static final int NAME_NO_MATCH = -1;
 
-    @Override
-    public Set<? extends SearchResult> loadInBackground() {
-        final Set<SearchResult> results = new HashSet<>();
-        results.addAll(buildPhysicalKeyboardSearchResults());
-        results.addAll(buildVirtualKeyboardSearchResults());
-        return results;
-    }
+        private final Context mContext;
+        private final SiteMapManager mSiteMapManager;
+        private final InputManager mInputManager;
+        private final InputMethodManager mImm;
+        private final PackageManager mPackageManager;
+        @VisibleForTesting
+        final String mQuery;
 
-    private Set<SearchResult> buildPhysicalKeyboardSearchResults() {
-        final Set<SearchResult> results = new HashSet<>();
-        final Context context = getContext();
-        final String screenTitle = context.getString(R.string.physical_keyboard_title);
+        private List<String> mPhysicalKeyboardBreadcrumb;
+        private List<String> mVirtualKeyboardBreadcrumb;
 
-        for (final InputDevice device : getPhysicalFullKeyboards()) {
-            final String deviceName = device.getName();
-            final int wordDiff = InstalledAppResultLoader.getWordDifference(deviceName, mQuery);
-            if (wordDiff == NAME_NO_MATCH) {
-                continue;
+        public InputDeviceResultCallable(Context context, String query, SiteMapManager mapManager) {
+            mContext = context;
+            mQuery = query;
+            mSiteMapManager = mapManager;
+            mInputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
+            mImm = (InputMethodManager) context.getSystemService(INPUT_METHOD_SERVICE);
+            mPackageManager = context.getPackageManager();
+        }
+
+        @Override
+        public List<? extends SearchResult> call() {
+            long startTime = System.currentTimeMillis();
+            final List<SearchResult> results = new ArrayList<>();
+            results.addAll(buildPhysicalKeyboardSearchResults());
+            results.addAll(buildVirtualKeyboardSearchResults());
+            Collections.sort(results);
+            Log.i(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 = InstalledAppResultLoader.getWordDifference(deviceName,
+                        mQuery);
+                if (wordDiff == NAME_NO_MATCH) {
+                    continue;
+                }
+                final String keyboardLayoutDescriptor = mInputManager
+                        .getCurrentKeyboardLayoutForInputDevice(device.getIdentifier());
+                final KeyboardLayout keyboardLayout = (keyboardLayoutDescriptor != null)
+                        ? mInputManager.getKeyboardLayout(keyboardLayoutDescriptor) : null;
+                final String summary = (keyboardLayout != null)
+                        ? keyboardLayout.toString()
+                        : mContext.getString(R.string.keyboard_layout_default_label);
+
+                final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(mContext,
+                        PHYSICAL_KEYBOARD_FRAGMENT, deviceName, screenTitle);
+                results.add(new SearchResult.Builder()
+                        .setTitle(deviceName)
+                        .setPayload(new ResultPayload(intent))
+                        .setStableId(Objects.hash(PHYSICAL_KEYBOARD_FRAGMENT, deviceName))
+                        .setSummary(summary)
+                        .setRank(wordDiff)
+                        .addBreadcrumbs(getPhysicalKeyboardBreadCrumb())
+                        .build());
             }
-            final String keyboardLayoutDescriptor = mInputManager
-                    .getCurrentKeyboardLayoutForInputDevice(device.getIdentifier());
-            final KeyboardLayout keyboardLayout = (keyboardLayoutDescriptor != null)
-                    ? mInputManager.getKeyboardLayout(keyboardLayoutDescriptor) : null;
-            final String summary = (keyboardLayout != null)
-                    ? keyboardLayout.toString()
-                    : context.getString(R.string.keyboard_layout_default_label);
-            final String key = deviceName;
-
-            final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(context,
-                    PHYSICAL_KEYBOARD_FRAGMENT, key, screenTitle);
-            results.add(new SearchResult.Builder()
-                    .setTitle(deviceName)
-                    .setPayload(new ResultPayload(intent))
-                    .setStableId(Objects.hash(PHYSICAL_KEYBOARD_FRAGMENT, key))
-                    .setSummary(summary)
-                    .setRank(wordDiff)
-                    .addBreadcrumbs(getPhysicalKeyboardBreadCrumb())
-                    .build());
+            return results;
         }
-        return results;
-    }
 
-    private Set<SearchResult> buildVirtualKeyboardSearchResults() {
-        final Set<SearchResult> results = new HashSet<>();
-        final Context context = getContext();
-        final String screenTitle = context.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 = InputMethodAndSubtypeUtil
-                    .getSubtypeLocaleNameListAsSentence(getAllSubtypesOf(info), context, info);
-            int wordDiff = InstalledAppResultLoader.getWordDifference(title, mQuery);
-            if (wordDiff == NAME_NO_MATCH) {
-                wordDiff = InstalledAppResultLoader.getWordDifference(summary, mQuery);
+        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 = InputMethodAndSubtypeUtil
+                        .getSubtypeLocaleNameListAsSentence(getAllSubtypesOf(info), mContext, info);
+                int wordDiff = InstalledAppResultLoader.getWordDifference(title, mQuery);
+                if (wordDiff == NAME_NO_MATCH) {
+                    wordDiff = InstalledAppResultLoader.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.buildSearchResultPageIntent(mContext,
+                        VIRTUAL_KEYBOARD_FRAGMENT, key, screenTitle);
+                results.add(new SearchResult.Builder()
+                        .setTitle(title)
+                        .setSummary(summary)
+                        .setRank(wordDiff)
+                        .setStableId(Objects.hash(VIRTUAL_KEYBOARD_FRAGMENT, key))
+                        .addBreadcrumbs(getVirtualKeyboardBreadCrumb())
+                        .setPayload(new ResultPayload(intent))
+                        .build());
             }
-            if (wordDiff == NAME_NO_MATCH) {
-                continue;
+            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));
             }
-            final ServiceInfo serviceInfo = info.getServiceInfo();
-            final String key = new ComponentName(serviceInfo.packageName, serviceInfo.name)
-                    .flattenToString();
-            final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(context,
-                    VIRTUAL_KEYBOARD_FRAGMENT, key, screenTitle);
-            results.add(new SearchResult.Builder()
-                    .setTitle(title)
-                    .setSummary(summary)
-                    .setRank(wordDiff)
-                    .setStableId(Objects.hash(VIRTUAL_KEYBOARD_FRAGMENT, key))
-                    .addBreadcrumbs(getVirtualKeyboardBreadCrumb())
-                    .setPayload(new ResultPayload(intent))
-                    .build());
+            return mPhysicalKeyboardBreadcrumb;
         }
-        return results;
-    }
 
-    private List<String> getPhysicalKeyboardBreadCrumb() {
-        if (mPhysicalKeyboardBreadcrumb == null || mPhysicalKeyboardBreadcrumb.isEmpty()) {
-            final Context context = getContext();
-            mPhysicalKeyboardBreadcrumb = mSiteMapManager.buildBreadCrumb(
-                    context, PHYSICAL_KEYBOARD_FRAGMENT,
-                    context.getString(R.string.physical_keyboard_title));
+
+        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;
         }
-        return mPhysicalKeyboardBreadcrumb;
-    }
 
-
-    private List<String> getVirtualKeyboardBreadCrumb() {
-        if (mVirtualKeyboardBreadcrumb == null || mVirtualKeyboardBreadcrumb.isEmpty()) {
-            final Context context = getContext();
-            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 (device != null && !device.isVirtual() && device.isFullKeyboard()) {
-                    keyboards.add(device);
+        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 (device != null && !device.isVirtual() && device.isFullKeyboard()) {
+                        keyboards.add(device);
+                    }
                 }
             }
+            return keyboards;
         }
-        return keyboards;
-    }
 
-    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));
+        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;
         }
-        return allSubtypes;
     }
 }
diff --git a/src/com/android/settings/search/InstalledAppResultLoader.java b/src/com/android/settings/search/InstalledAppResultLoader.java
index 7645c15..e5d8ac1 100644
--- a/src/com/android/settings/search/InstalledAppResultLoader.java
+++ b/src/com/android/settings/search/InstalledAppResultLoader.java
@@ -29,124 +29,39 @@
 import android.provider.Settings;
 import android.support.annotation.VisibleForTesting;
 import android.text.TextUtils;
+import android.util.Log;
 
 import com.android.internal.logging.nano.MetricsProto;
 import com.android.settings.R;
 import com.android.settings.SettingsActivity;
 import com.android.settings.applications.manageapplications.ManageApplications;
 import com.android.settings.dashboard.SiteMapManager;
-import com.android.settings.utils.AsyncLoader;
 import com.android.settingslib.wrapper.PackageManagerWrapper;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
 
 /**
  * Search loader for installed apps.
  */
-public class InstalledAppResultLoader extends AsyncLoader<Set<? extends SearchResult>> {
+public class InstalledAppResultLoader extends FutureTask<List<? extends SearchResult>> {
+
+    private static final String TAG = "InstalledAppFutureTask";
 
     private static final int NAME_NO_MATCH = -1;
     private static final Intent LAUNCHER_PROBE = new Intent(Intent.ACTION_MAIN)
             .addCategory(Intent.CATEGORY_LAUNCHER);
 
-    private List<String> mBreadcrumb;
-    private SiteMapManager mSiteMapManager;
-    @VisibleForTesting
-    final String mQuery;
-    private final UserManager mUserManager;
-    private final PackageManagerWrapper mPackageManager;
-    private final List<ResolveInfo> mHomeActivities = new ArrayList<>();
-
-    public InstalledAppResultLoader(Context context, PackageManagerWrapper pmWrapper,
-            String query, SiteMapManager mapManager) {
-        super(context);
-        mSiteMapManager = mapManager;
-        mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
-        mPackageManager = pmWrapper;
-        mQuery = query;
-    }
-
-    @Override
-    public Set<? extends SearchResult> loadInBackground() {
-        final Set<AppSearchResult> results = new HashSet<>();
-        final PackageManager pm = mPackageManager.getPackageManager();
-
-        mHomeActivities.clear();
-        mPackageManager.getHomeActivities(mHomeActivities);
-
-        for (UserInfo user : getUsersToCount()) {
-            final List<ApplicationInfo> apps =
-                    mPackageManager.getInstalledApplicationsAsUser(
-                            PackageManager.MATCH_DISABLED_COMPONENTS
-                                    | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
-                                    | (user.isAdmin() ? PackageManager.MATCH_ANY_USER : 0),
-                            user.id);
-            for (ApplicationInfo info : apps) {
-                if (!shouldIncludeAsCandidate(info, user)) {
-                    continue;
-                }
-                final CharSequence label = info.loadLabel(pm);
-                final int wordDiff = getWordDifference(label.toString(), mQuery);
-                if (wordDiff == NAME_NO_MATCH) {
-                    continue;
-                }
-                final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
-                        .setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
-                        .setData(Uri.fromParts("package", info.packageName, null))
-                        .putExtra(SettingsActivity.EXTRA_SOURCE_METRICS_CATEGORY,
-                                MetricsProto.MetricsEvent.DASHBOARD_SEARCH_RESULTS);
-
-                final AppSearchResult.Builder builder = new AppSearchResult.Builder();
-                builder.setAppInfo(info)
-                        .setStableId(Objects.hash(info.packageName, user.id))
-                        .setTitle(info.loadLabel(pm))
-                        .setRank(getRank(wordDiff))
-                        .addBreadcrumbs(getBreadCrumb())
-                        .setPayload(new ResultPayload(intent));
-                results.add(builder.build());
-            }
-        }
-        return results;
-    }
-
-    /**
-     * Returns true if the candidate should be included in candidate list
-     * <p/>
-     * This method matches logic in {@code ApplicationState#FILTER_DOWNLOADED_AND_LAUNCHER}.
-     */
-    private boolean shouldIncludeAsCandidate(ApplicationInfo info, UserInfo user) {
-        // Not system app
-        if ((info.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0
-                || (info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
-            return true;
-        }
-        // Shows up in launcher
-        final Intent launchIntent = new Intent(LAUNCHER_PROBE)
-                .setPackage(info.packageName);
-        final List<ResolveInfo> intents = mPackageManager.queryIntentActivitiesAsUser(
-                launchIntent,
-                PackageManager.MATCH_DISABLED_COMPONENTS
-                        | PackageManager.MATCH_DIRECT_BOOT_AWARE
-                        | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
-                user.id);
-        if (intents != null && intents.size() != 0) {
-            return true;
-        }
-        // Is launcher app itself
-        return isPackageInList(mHomeActivities, info.packageName);
-    }
-
-    @Override
-    protected void onDiscardResult(Set<? extends SearchResult> result) {
-
-    }
-
-    private List<UserInfo> getUsersToCount() {
-        return mUserManager.getProfiles(UserHandle.myUserId());
+    public InstalledAppResultLoader(Context context, PackageManagerWrapper wrapper,
+            String query, SiteMapManager manager) {
+        super(new InstalledAppResultCallable(context, wrapper, query, manager));
     }
 
     /**
@@ -213,35 +128,133 @@
         return NAME_NO_MATCH;
     }
 
-    private boolean isPackageInList(List<ResolveInfo> resolveInfos, String pkg) {
-        for (ResolveInfo info : resolveInfos) {
-            if (TextUtils.equals(info.activityInfo.packageName, pkg)) {
+    static class InstalledAppResultCallable implements
+            Callable<List<? extends SearchResult>> {
+
+        private final Context mContext;
+        private List<String> mBreadcrumb;
+        private SiteMapManager mSiteMapManager;
+        @VisibleForTesting
+        final String mQuery;
+        private final UserManager mUserManager;
+        private final PackageManagerWrapper mPackageManager;
+        private final List<ResolveInfo> mHomeActivities = new ArrayList<>();
+
+        public InstalledAppResultCallable(Context context, PackageManagerWrapper pmWrapper,
+                String query, SiteMapManager mapManager) {
+            mContext = context;
+            mSiteMapManager = mapManager;
+            mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+            mPackageManager = pmWrapper;
+            mQuery = query;
+        }
+
+        @Override
+        public List<? extends SearchResult> call() throws Exception {
+            long startTime = System.currentTimeMillis();
+            final List<AppSearchResult> results = new ArrayList<>();
+            final PackageManager pm = mPackageManager.getPackageManager();
+
+            mHomeActivities.clear();
+            mPackageManager.getHomeActivities(mHomeActivities);
+
+            for (UserInfo user : getUsersToCount()) {
+                final List<ApplicationInfo> apps =
+                        mPackageManager.getInstalledApplicationsAsUser(
+                                PackageManager.MATCH_DISABLED_COMPONENTS
+                                        | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
+                                        | (user.isAdmin() ? PackageManager.MATCH_ANY_USER : 0),
+                                user.id);
+                for (ApplicationInfo info : apps) {
+                    if (!shouldIncludeAsCandidate(info, user)) {
+                        continue;
+                    }
+                    final CharSequence label = info.loadLabel(pm);
+                    final int wordDiff = getWordDifference(label.toString(), mQuery);
+                    if (wordDiff == NAME_NO_MATCH) {
+                        continue;
+                    }
+                    final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+                            .setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+                            .setData(Uri.fromParts("package", info.packageName, null))
+                            .putExtra(SettingsActivity.EXTRA_SOURCE_METRICS_CATEGORY,
+                                    MetricsProto.MetricsEvent.DASHBOARD_SEARCH_RESULTS);
+
+                    final AppSearchResult.Builder builder = new AppSearchResult.Builder();
+                    builder.setAppInfo(info)
+                            .setStableId(Objects.hash(info.packageName, user.id))
+                            .setTitle(info.loadLabel(pm))
+                            .setRank(getRank(wordDiff))
+                            .addBreadcrumbs(getBreadCrumb())
+                            .setPayload(new ResultPayload(intent));
+                    results.add(builder.build());
+                }
+            }
+            Collections.sort(results);
+            Log.i(TAG, "App search loading took:" + (System.currentTimeMillis() - startTime));
+            return results;
+        }
+
+        /**
+         * Returns true if the candidate should be included in candidate list
+         * <p/>
+         * This method matches logic in {@code ApplicationState#FILTER_DOWNLOADED_AND_LAUNCHER}.
+         */
+        private boolean shouldIncludeAsCandidate(ApplicationInfo info, UserInfo user) {
+            // Not system app
+            if ((info.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0
+                    || (info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
                 return true;
             }
+            // Shows up in launcher
+            final Intent launchIntent = new Intent(LAUNCHER_PROBE)
+                    .setPackage(info.packageName);
+            final List<ResolveInfo> intents = mPackageManager.queryIntentActivitiesAsUser(
+                    launchIntent,
+                    PackageManager.MATCH_DISABLED_COMPONENTS
+                            | PackageManager.MATCH_DIRECT_BOOT_AWARE
+                            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
+                    user.id);
+            if (intents != null && intents.size() != 0) {
+                return true;
+            }
+            // Is launcher app itself
+            return isPackageInList(mHomeActivities, info.packageName);
         }
-        return false;
-    }
 
-    private List<String> getBreadCrumb() {
-        if (mBreadcrumb == null || mBreadcrumb.isEmpty()) {
-            final Context context = getContext();
-            mBreadcrumb = mSiteMapManager.buildBreadCrumb(
-                    context, ManageApplications.class.getName(),
-                    context.getString(R.string.applications_settings));
+        private List<UserInfo> getUsersToCount() {
+            return mUserManager.getProfiles(UserHandle.myUserId());
         }
-        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;
+        private boolean isPackageInList(List<ResolveInfo> resolveInfos, String pkg) {
+            for (ResolveInfo info : resolveInfos) {
+                if (TextUtils.equals(info.activityInfo.packageName, pkg)) {
+                    return true;
+                }
+            }
+            return false;
         }
-        return 3;
+
+        private List<String> getBreadCrumb() {
+            if (mBreadcrumb == null || mBreadcrumb.isEmpty()) {
+                mBreadcrumb = mSiteMapManager.buildBreadCrumb(
+                        mContext, ManageApplications.class.getName(),
+                        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/search/SearchFeatureProvider.java b/src/com/android/settings/search/SearchFeatureProvider.java
index 4df8203..42afee9 100644
--- a/src/com/android/settings/search/SearchFeatureProvider.java
+++ b/src/com/android/settings/search/SearchFeatureProvider.java
@@ -19,10 +19,14 @@
 import android.annotation.NonNull;
 import android.content.ComponentName;
 import android.content.Context;
+import android.util.Pair;
 import android.view.View;
 
 import com.android.settings.dashboard.SiteMapManager;
-import com.android.settings.search.ranking.SearchResultsRankerCallback;
+
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.FutureTask;
 
 /**
  * FeatureProvider for Settings Search
@@ -44,25 +48,30 @@
             throws SecurityException, IllegalArgumentException;
 
     /**
+     * Returns a new loader to get settings search results.
+     */
+    SearchResultLoader getSearchResultLoader(Context context, String query);
+
+    /**
      * Returns a new loader to search in index database.
      */
-    DatabaseResultLoader getDatabaseSearchLoader(Context context, String query);
+    DatabaseResultLoader getStaticSearchResultTask(Context context, String query);
 
     /**
      * Returns a new loader to search installed apps.
      */
-    InstalledAppResultLoader getInstalledAppSearchLoader(Context context, String query);
+    InstalledAppResultLoader getInstalledAppSearchTask(Context context, String query);
 
     /**
      * Returns a new loader to search accessibility services.
      */
-    AccessibilityServiceResultLoader getAccessibilityServiceResultLoader(Context context,
+    AccessibilityServiceResultLoader getAccessibilityServiceResultTask(Context context,
             String query);
 
     /**
      * Returns a new loader to search input devices.
      */
-    InputDeviceResultLoader getInputDeviceResultLoader(Context context, String query);
+    InputDeviceResultLoader getInputDeviceResultTask(Context context, String query);
 
     /**
      * Returns a new loader to get all recently saved queries search terms.
@@ -96,6 +105,11 @@
     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.
      */
     default void initFeedbackButton() {
@@ -115,23 +129,6 @@
     }
 
     /**
-     * Query search results based on the input query.
-     *
-     * @param context                     application context
-     * @param query                       input user query
-     * @param searchResultsRankerCallback {@link SearchResultsRankerCallback}
-     */
-    default void querySearchResults(Context context, String query,
-            SearchResultsRankerCallback searchResultsRankerCallback) {
-    }
-
-    /**
-     * Cancel pending search query
-     */
-    default void cancelPendingSearchQuery(Context context) {
-    }
-
-    /**
      * Notify that a search result is clicked.
      *
      * @param context      application context
@@ -161,4 +158,10 @@
     default void searchRankingWarmup(Context context) {
     }
 
+    /**
+     * Return a FutureTask to get a list of scores for search results.
+     */
+    default FutureTask<List<Pair<String, Float>>> getRankerTask(Context context, String query) {
+        return null;
+    }
 }
diff --git a/src/com/android/settings/search/SearchFeatureProviderImpl.java b/src/com/android/settings/search/SearchFeatureProviderImpl.java
index af7f177..e0fbfd7 100644
--- a/src/com/android/settings/search/SearchFeatureProviderImpl.java
+++ b/src/com/android/settings/search/SearchFeatureProviderImpl.java
@@ -22,12 +22,15 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.settings.dashboard.SiteMapManager;
 import com.android.settings.overlay.FeatureFactory;
 import com.android.settings.search.indexing.IndexData;
 import com.android.settingslib.wrapper.PackageManagerWrapper;
 
 import java.util.Locale;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 /**
  * FeatureProvider for the refactored search code.
@@ -40,6 +43,7 @@
 
     private DatabaseIndexingManager mDatabaseIndexingManager;
     private SiteMapManager mSiteMapManager;
+    private ExecutorService mExecutorService;
 
     @Override
     public boolean isEnabled(Context context) {
@@ -59,26 +63,31 @@
     }
 
     @Override
-    public DatabaseResultLoader getDatabaseSearchLoader(Context context, String query) {
+    public SearchResultLoader getSearchResultLoader(Context context, String query) {
+        return new SearchResultLoader(context, cleanQuery(query));
+    }
+
+    @Override
+    public DatabaseResultLoader getStaticSearchResultTask(Context context, String query) {
         return new DatabaseResultLoader(context, cleanQuery(query), getSiteMapManager());
     }
 
     @Override
-    public InstalledAppResultLoader getInstalledAppSearchLoader(Context context, String query) {
+    public InstalledAppResultLoader getInstalledAppSearchTask(Context context, String query) {
         return new InstalledAppResultLoader(
                 context, new PackageManagerWrapper(context.getPackageManager()),
                 cleanQuery(query), getSiteMapManager());
     }
 
     @Override
-    public AccessibilityServiceResultLoader getAccessibilityServiceResultLoader(Context context,
+    public AccessibilityServiceResultLoader getAccessibilityServiceResultTask(Context context,
             String query) {
         return new AccessibilityServiceResultLoader(context, cleanQuery(query),
                 getSiteMapManager());
     }
 
     @Override
-    public InputDeviceResultLoader getInputDeviceResultLoader(Context context, String query) {
+    public InputDeviceResultLoader getInputDeviceResultTask(Context context, String query) {
         return new InputDeviceResultLoader(context, cleanQuery(query), getSiteMapManager());
     }
 
@@ -124,12 +133,21 @@
                 .histogram(context, METRICS_ACTION_SETTINGS_INDEX, indexingTime);
     }
 
+    @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
      */
-    private String cleanQuery(String query) {
+    @VisibleForTesting
+    String cleanQuery(String query) {
         if (TextUtils.isEmpty(query)) {
             return null;
         }
diff --git a/src/com/android/settings/search/SearchFragment.java b/src/com/android/settings/search/SearchFragment.java
index ca951c6..e6316a8 100644
--- a/src/com/android/settings/search/SearchFragment.java
+++ b/src/com/android/settings/search/SearchFragment.java
@@ -54,8 +54,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * This fragment manages the lifecycle of indexing and searching.
@@ -68,7 +66,7 @@
  * the query if the user has entered text.
  */
 public class SearchFragment extends InstrumentedFragment implements SearchView.OnQueryTextListener,
-        LoaderManager.LoaderCallbacks<Set<? extends SearchResult>>, IndexingCallback {
+        LoaderManager.LoaderCallbacks<List<? extends SearchResult>>, IndexingCallback {
     private static final String TAG = "SearchFragment";
 
     // State values
@@ -78,23 +76,14 @@
 
     static final class SearchLoaderId {
         // Search Query IDs
-        public static final int DATABASE = 1;
-        public static final int INSTALLED_APPS = 2;
-        public static final int ACCESSIBILITY_SERVICES = 3;
-        public static final int INPUT_DEVICES = 4;
+        public static final int SEARCH_RESULT = 1;
 
         // Saved Query IDs
-        public static final int SAVE_QUERY_TASK = 5;
-        public static final int REMOVE_QUERY_TASK = 6;
-        public static final int SAVED_QUERIES = 7;
+        public static final int SAVE_QUERY_TASK = 2;
+        public static final int REMOVE_QUERY_TASK = 3;
+        public static final int SAVED_QUERIES = 4;
     }
 
-
-    private static final int NUM_QUERY_LOADERS = 4;
-
-    @VisibleForTesting
-    AtomicInteger mUnfinishedLoadersCount = new AtomicInteger(NUM_QUERY_LOADERS);
-
     @VisibleForTesting
     String mQuery;
 
@@ -147,7 +136,7 @@
         setHasOptionsMenu(true);
 
         final LoaderManager loaderManager = getLoaderManager();
-        mSearchAdapter = new SearchResultsAdapter(this, mSearchFeatureProvider);
+        mSearchAdapter = new SearchResultsAdapter(this /* fragment */);
         mSavedQueryController = new SavedQueryController(
                 getContext(), loaderManager, mSearchAdapter);
         mSearchFeatureProvider.initFeedbackButton();
@@ -277,15 +266,11 @@
 
         if (isEmptyQuery) {
             final LoaderManager loaderManager = getLoaderManager();
-            loaderManager.destroyLoader(SearchLoaderId.DATABASE);
-            loaderManager.destroyLoader(SearchLoaderId.INSTALLED_APPS);
-            loaderManager.destroyLoader(SearchLoaderId.ACCESSIBILITY_SERVICES);
-            loaderManager.destroyLoader(SearchLoaderId.INPUT_DEVICES);
+            loaderManager.destroyLoader(SearchLoaderId.SEARCH_RESULT);
             mShowingSavedQuery = true;
             mSavedQueryController.loadSavedQueries();
             mSearchFeatureProvider.hideFeedbackButton();
         } else {
-            mSearchAdapter.initializeSearch(mQuery);
             restartLoaders();
         }
 
@@ -301,35 +286,25 @@
     }
 
     @Override
-    public Loader<Set<? extends SearchResult>> onCreateLoader(int id, Bundle args) {
+    public Loader<List<? extends SearchResult>> onCreateLoader(int id, Bundle args) {
         final Activity activity = getActivity();
 
-        switch (id) {
-            case SearchLoaderId.DATABASE:
-                return mSearchFeatureProvider.getDatabaseSearchLoader(activity, mQuery);
-            case SearchLoaderId.INSTALLED_APPS:
-                return mSearchFeatureProvider.getInstalledAppSearchLoader(activity, mQuery);
-            case SearchLoaderId.ACCESSIBILITY_SERVICES:
-                return mSearchFeatureProvider.getAccessibilityServiceResultLoader(activity, mQuery);
-            case SearchLoaderId.INPUT_DEVICES:
-                return mSearchFeatureProvider.getInputDeviceResultLoader(activity, mQuery);
+        switch(id) {
+            case SearchLoaderId.SEARCH_RESULT:
+                return mSearchFeatureProvider.getSearchResultLoader(activity, mQuery);
             default:
                 return null;
         }
     }
 
     @Override
-    public void onLoadFinished(Loader<Set<? extends SearchResult>> loader,
-            Set<? extends SearchResult> data) {
-        mSearchAdapter.addSearchResults(data, loader.getClass().getName());
-        if (mUnfinishedLoadersCount.decrementAndGet() != 0) {
-            return;
-        }
-        mSearchAdapter.notifyResultsLoaded();
+    public void onLoadFinished(Loader<List<? extends SearchResult>> loader,
+            List<? extends SearchResult> data) {
+        mSearchAdapter.postSearchResults(data);
     }
 
     @Override
-    public void onLoaderReset(Loader<Set<? extends SearchResult>> loader) {
+    public void onLoaderReset(Loader<List<? extends SearchResult>> loader) {
     }
 
     /**
@@ -344,13 +319,8 @@
             mSavedQueryController.loadSavedQueries();
         } else {
             final LoaderManager loaderManager = getLoaderManager();
-            loaderManager.initLoader(SearchLoaderId.DATABASE, null /* args */, this /* callback */);
-            loaderManager.initLoader(
-                    SearchLoaderId.INSTALLED_APPS, null /* args */, this /* callback */);
-            loaderManager.initLoader(
-                    SearchLoaderId.ACCESSIBILITY_SERVICES, null /* args */, this /* callback */);
-            loaderManager.initLoader(
-                    SearchLoaderId.INPUT_DEVICES, null /* args */, this /* callback */);
+            loaderManager.initLoader(SearchLoaderId.SEARCH_RESULT, null /* args */,
+                    this /* callback */);
         }
 
         requery();
@@ -388,15 +358,8 @@
     private void restartLoaders() {
         mShowingSavedQuery = false;
         final LoaderManager loaderManager = getLoaderManager();
-        mUnfinishedLoadersCount.set(NUM_QUERY_LOADERS);
         loaderManager.restartLoader(
-                SearchLoaderId.DATABASE, null /* args */, this /* callback */);
-        loaderManager.restartLoader(
-                SearchLoaderId.INSTALLED_APPS, null /* args */, this /* callback */);
-        loaderManager.restartLoader(
-                SearchLoaderId.ACCESSIBILITY_SERVICES, null /* args */, this /* callback */);
-        loaderManager.restartLoader(
-                SearchLoaderId.INPUT_DEVICES, null /* args */, this /* callback */);
+                SearchLoaderId.SEARCH_RESULT, null /* args */, this /* callback */);
     }
 
     public String getQuery() {
@@ -453,9 +416,7 @@
         taggedData.add(Pair.create(
                 MetricsEvent.FIELD_SETTINGS_SEARCH_RESULT_RANK,
                 resultViewHolder.getAdapterPosition()));
-        taggedData.add(Pair.create(
-                MetricsEvent.FIELD_SETTINGS_SEARCH_RESULT_ASYNC_RANKING_STATE,
-                mSearchAdapter.getAsyncRankingState()));
+        // TODO (b/67744820) Move metrics to SettingsIntelligence (including ranking state).
         taggedData.add(Pair.create(
                 MetricsEvent.FIELD_SETTINGS_SEARCH_QUERY_LENGTH,
                 TextUtils.isEmpty(mQuery) ? 0 : mQuery.length()));
diff --git a/src/com/android/settings/search/SearchIndexableResources.java b/src/com/android/settings/search/SearchIndexableResources.java
index cb39970..69b2f9f 100644
--- a/src/com/android/settings/search/SearchIndexableResources.java
+++ b/src/com/android/settings/search/SearchIndexableResources.java
@@ -23,7 +23,6 @@
 import com.android.settings.DateTimeSettings;
 import com.android.settings.DeviceInfoSettings;
 import com.android.settings.DisplaySettings;
-import com.android.settings.EncryptionAndCredential;
 import com.android.settings.LegalSettings;
 import com.android.settings.ScreenPinningSettings;
 import com.android.settings.SecuritySettings;
@@ -75,6 +74,7 @@
 import com.android.settings.notification.ZenModeBehaviorSettings;
 import com.android.settings.notification.ZenModeSettings;
 import com.android.settings.print.PrintSettingsFragment;
+import com.android.settings.security.EncryptionAndCredential;
 import com.android.settings.security.LockscreenDashboardFragment;
 import com.android.settings.sim.SimSettings;
 import com.android.settings.support.SupportDashboardActivity;
diff --git a/src/com/android/settings/search/SearchResultAggregator.java b/src/com/android/settings/search/SearchResultAggregator.java
new file mode 100644
index 0000000..890e3f0
--- /dev/null
+++ b/src/com/android/settings/search/SearchResultAggregator.java
@@ -0,0 +1,177 @@
+package com.android.settings.search;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.settings.overlay.FeatureFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+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.
+ *
+ * TODO (b/64939692) Convert the timing logs to metrics
+ */
+public class SearchResultAggregator {
+
+    private static final String TAG = "SearchResultAggregator";
+
+    /**
+     * Timeout for first task. Allows for longer delay.
+     */
+    private static final long LONG_CHECK_TASK_TIMEOUT_MS = 500;
+
+    /**
+     * Timeout for subsequent tasks to allow for fast returning tasks.
+     */
+    private static final long SHORT_CHECK_TASK_TIMEOUT_MS = 150;
+
+    private static SearchResultAggregator sResultAggregator;
+
+    // TODO (b/33577327) Merge the other loaders into a single dynamic loader
+    static final class ResultLoaderId {
+        static final int STATIC_RESULTS = 1;
+        static final int INSTALLED_RESULTS = 2;
+        static final int INPUT_RESULTS = 3;
+        static final int ACCESSIBILITY_RESULTS = 4;
+    }
+
+    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) {
+        SearchFeatureProvider mFeatureProvider = FeatureFactory.getFactory(
+                context).getSearchFeatureProvider();
+        ExecutorService executorService = mFeatureProvider.getExecutorService();
+
+        final DatabaseResultLoader staticResultsTask =
+                mFeatureProvider.getStaticSearchResultTask(context, query);
+        final InstalledAppResultLoader installedAppTask =
+                mFeatureProvider.getInstalledAppSearchTask(context, query);
+        final InputDeviceResultLoader inputDevicesTask =
+                mFeatureProvider.getInputDeviceResultTask(context, query);
+        final AccessibilityServiceResultLoader accessibilityServicesTask =
+                mFeatureProvider.getAccessibilityServiceResultTask(context,
+                        query);
+
+        executorService.execute(staticResultsTask);
+        executorService.execute(installedAppTask);
+        executorService.execute(inputDevicesTask);
+        executorService.execute(accessibilityServicesTask);
+
+        SparseArray<List<? extends SearchResult>> resultsArray = new SparseArray<>();
+        List<? extends SearchResult> EMPTY_LIST = new ArrayList<>();
+
+        long allTasksStart = System.currentTimeMillis();
+        try {
+            resultsArray.put(ResultLoaderId.INPUT_RESULTS,
+                    inputDevicesTask.get(SHORT_CHECK_TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        } catch (TimeoutException | InterruptedException | ExecutionException e) {
+            Log.d(TAG, "Could not retrieve input devices results in time: " + e);
+            resultsArray.put(ResultLoaderId.INPUT_RESULTS, EMPTY_LIST);
+        }
+
+        try {
+            resultsArray.put(ResultLoaderId.ACCESSIBILITY_RESULTS,
+                    accessibilityServicesTask.get(SHORT_CHECK_TASK_TIMEOUT_MS,
+                            TimeUnit.MILLISECONDS));
+        } catch (TimeoutException | InterruptedException | ExecutionException e) {
+            Log.d(TAG, "Could not retrieve accessibility results in time: " + e);
+            resultsArray.put(ResultLoaderId.ACCESSIBILITY_RESULTS, EMPTY_LIST);
+        }
+
+        try {
+            resultsArray.put(ResultLoaderId.STATIC_RESULTS,
+                    staticResultsTask.get(LONG_CHECK_TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        } catch (TimeoutException | InterruptedException | ExecutionException e) {
+            Log.d(TAG, "Could not retrieve static results: " + e);
+            resultsArray.put(ResultLoaderId.STATIC_RESULTS, EMPTY_LIST);
+        }
+
+        try {
+            resultsArray.put(ResultLoaderId.INSTALLED_RESULTS,
+                    installedAppTask.get(SHORT_CHECK_TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        } catch (TimeoutException | InterruptedException | ExecutionException e) {
+            Log.d(TAG, "Could not retrieve installed app results in time: " + e);
+
+            resultsArray.put(ResultLoaderId.INSTALLED_RESULTS, EMPTY_LIST);
+        }
+
+        long mergeStartTime = System.currentTimeMillis();
+        Log.i(TAG, "Total result loader time: " + (mergeStartTime - allTasksStart));
+        List<? extends SearchResult> mergedResults = mergeSearchResults(resultsArray);
+        Log.i(TAG, "Total merge time: " + (System.currentTimeMillis() - mergeStartTime));
+        Log.i(TAG, "Total aggregator time: " + (System.currentTimeMillis() - allTasksStart));
+
+        return mergedResults;
+    }
+
+    // TODO (b/68255021) scale the dynamic search results ranks and do a k-way merge
+    private List<? extends SearchResult> mergeSearchResults(
+            SparseArray<List<? extends SearchResult>> resultsArray) {
+        List<? extends SearchResult> staticResults = resultsArray.get(
+                ResultLoaderId.STATIC_RESULTS);
+        List<? extends SearchResult> installedAppResults = resultsArray.get(
+                ResultLoaderId.INSTALLED_RESULTS);
+        List<? extends SearchResult> accessibilityResults = resultsArray.get(
+                ResultLoaderId.ACCESSIBILITY_RESULTS);
+        List<? extends SearchResult> inputDeviceResults = resultsArray.get(
+                ResultLoaderId.INPUT_RESULTS);
+        List<SearchResult> searchResults;
+
+        int staticSize = staticResults.size();
+        int appSize = installedAppResults.size();
+        int a11ySize = accessibilityResults.size();
+        int inputDeviceSize = inputDeviceResults.size();
+        int appIndex = 0;
+        int a11yIndex = 0;
+        int inputDeviceIndex = 0;
+        int rank = SearchResult.TOP_RANK;
+
+        // TODO: We need a helper method to do k-way merge.
+        searchResults = new ArrayList<>(staticSize + appSize + a11ySize + inputDeviceSize);
+        searchResults.addAll(resultsArray.get(ResultLoaderId.STATIC_RESULTS));
+
+        while (rank <= SearchResult.BOTTOM_RANK) {
+            while ((appIndex < appSize) && (installedAppResults.get(appIndex).rank == rank)) {
+                searchResults.add(installedAppResults.get(appIndex++));
+            }
+            while ((a11yIndex < a11ySize) && (accessibilityResults.get(a11yIndex).rank == rank)) {
+                searchResults.add(accessibilityResults.get(a11yIndex++));
+            }
+            while (inputDeviceIndex < inputDeviceSize
+                    && inputDeviceResults.get(inputDeviceIndex).rank == rank) {
+                searchResults.add(inputDeviceResults.get(inputDeviceIndex++));
+            }
+            rank++;
+        }
+
+        while (appIndex < appSize) {
+            searchResults.add(installedAppResults.get(appIndex++));
+        }
+        while (a11yIndex < a11ySize) {
+            searchResults.add(accessibilityResults.get(a11yIndex++));
+        }
+        while (inputDeviceIndex < inputDeviceSize) {
+            searchResults.add(inputDeviceResults.get(inputDeviceIndex++));
+        }
+
+        return searchResults;
+    }
+}
diff --git a/src/com/android/settings/search/SearchResultDiffCallback.java b/src/com/android/settings/search/SearchResultDiffCallback.java
index b7bbc66..0f0b977 100644
--- a/src/com/android/settings/search/SearchResultDiffCallback.java
+++ b/src/com/android/settings/search/SearchResultDiffCallback.java
@@ -26,10 +26,11 @@
  */
 public class SearchResultDiffCallback extends DiffUtil.Callback {
 
-    private List<SearchResult> mOldList;
-    private List<SearchResult> mNewList;
+    private List<? extends SearchResult> mOldList;
+    private List<? extends SearchResult> mNewList;
 
-    public SearchResultDiffCallback(List<SearchResult> oldList, List<SearchResult> newList) {
+    public SearchResultDiffCallback(List<? extends SearchResult> oldList,
+            List<? extends SearchResult> newList) {
         mOldList = oldList;
         mNewList = newList;
     }
diff --git a/src/com/android/settings/search/SearchResultLoader.java b/src/com/android/settings/search/SearchResultLoader.java
new file mode 100644
index 0000000..7ec3146
--- /dev/null
+++ b/src/com/android/settings/search/SearchResultLoader.java
@@ -0,0 +1,30 @@
+package com.android.settings.search;
+
+import com.android.settings.utils.AsyncLoader;
+
+import android.content.Context;
+
+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/search/SearchResultsAdapter.java b/src/com/android/settings/search/SearchResultsAdapter.java
index 5fedc52..c05ce18 100644
--- a/src/com/android/settings/search/SearchResultsAdapter.java
+++ b/src/com/android/settings/search/SearchResultsAdapter.java
@@ -18,87 +18,25 @@
 package com.android.settings.search;
 
 import android.content.Context;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.support.annotation.IntDef;
-import android.support.annotation.MainThread;
-import android.support.annotation.VisibleForTesting;
 import android.support.v7.util.DiffUtil;
 import android.support.v7.widget.RecyclerView;
-import android.util.ArrayMap;
-import android.util.Log;
-import android.util.Pair;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 
 import com.android.settings.R;
-import com.android.settings.search.ranking.SearchResultsRankerCallback;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeSet;
 
-public class SearchResultsAdapter extends RecyclerView.Adapter<SearchViewHolder>
-        implements SearchResultsRankerCallback {
-    private static final String TAG = "SearchResultsAdapter";
-
-    @VisibleForTesting
-    static final String DB_RESULTS_LOADER_KEY = DatabaseResultLoader.class.getName();
-
-    @VisibleForTesting
-    static final String APP_RESULTS_LOADER_KEY = InstalledAppResultLoader.class.getName();
-    @VisibleForTesting
-    static final String ACCESSIBILITY_LOADER_KEY = AccessibilityServiceResultLoader.class.getName();
-    @VisibleForTesting
-    static final String INPUT_DEVICE_LOADER_KEY = InputDeviceResultLoader.class.getName();
-
-    @VisibleForTesting
-    static final int MSG_RANKING_TIMED_OUT = 1;
+public class SearchResultsAdapter extends RecyclerView.Adapter<SearchViewHolder> {
 
     private final SearchFragment mFragment;
-    private final Context mContext;
     private final List<SearchResult> mSearchResults;
-    private final List<SearchResult> mStaticallyRankedSearchResults;
-    private Map<String, Set<? extends SearchResult>> mResultsMap;
-    private final SearchFeatureProvider mSearchFeatureProvider;
-    private List<Pair<String, Float>> mSearchRankingScores;
-    private Handler mHandler;
-    private boolean mSearchResultsLoaded;
-    private boolean mSearchResultsUpdated;
 
-    @IntDef({DISABLED, PENDING_RESULTS, SUCCEEDED, FAILED, TIMED_OUT})
-    @Retention(RetentionPolicy.SOURCE)
-    private @interface AsyncRankingState {}
-    @VisibleForTesting
-    static final int DISABLED = 0;
-    @VisibleForTesting
-    static final int PENDING_RESULTS = 1;
-    @VisibleForTesting
-    static final int SUCCEEDED = 2;
-    @VisibleForTesting
-    static final int FAILED = 3;
-    @VisibleForTesting
-    static final int TIMED_OUT = 4;
-    private @AsyncRankingState int mAsyncRankingState;
-
-    public SearchResultsAdapter(SearchFragment fragment,
-            SearchFeatureProvider searchFeatureProvider) {
+    public SearchResultsAdapter(SearchFragment fragment) {
         mFragment = fragment;
-        mContext = fragment.getContext().getApplicationContext();
         mSearchResults = new ArrayList<>();
-        mResultsMap = new ArrayMap<>();
-        mSearchRankingScores = new ArrayList<>();
-        mStaticallyRankedSearchResults = new ArrayList<>();
-        mSearchFeatureProvider = searchFeatureProvider;
 
         setHasStableIds(true);
     }
@@ -149,298 +87,30 @@
         return mSearchResults.size();
     }
 
-    @MainThread
-    @Override
-    public void onRankingScoresAvailable(List<Pair<String, Float>> searchRankingScores) {
-        // Received the scores, stop the timeout timer.
-        getHandler().removeMessages(MSG_RANKING_TIMED_OUT);
-        if (mAsyncRankingState == PENDING_RESULTS) {
-            mAsyncRankingState = SUCCEEDED;
-            mSearchRankingScores.clear();
-            mSearchRankingScores.addAll(searchRankingScores);
-            if (canUpdateSearchResults()) {
-                updateSearchResults();
-            }
-        } else {
-            Log.w(TAG, "Ranking scores became available in invalid state: " + mAsyncRankingState);
-        }
-    }
-
-    @MainThread
-    @Override
-    public void onRankingFailed() {
-        if (mAsyncRankingState == PENDING_RESULTS) {
-            mAsyncRankingState = FAILED;
-            if (canUpdateSearchResults()) {
-                updateSearchResults();
-            }
-        } else {
-            Log.w(TAG, "Ranking scores failed in invalid states: " + mAsyncRankingState);
-        }
-    }
-
-   /**
-     * Store the results from each of the loaders to be merged when all loaders are finished.
-     *
-     * @param results         the results from the loader.
-     * @param loaderClassName class name of the loader.
-     */
-    @MainThread
-    public void addSearchResults(Set<? extends SearchResult> results, String loaderClassName) {
-        if (results == null) {
-            return;
-        }
-        mResultsMap.put(loaderClassName, results);
-    }
-
     /**
      * Displays recent searched queries.
-     *
-     * @return The number of saved queries to display
      */
-    public int displaySavedQuery(List<? extends SearchResult> data) {
+    public void displaySavedQuery(List<? extends SearchResult> data) {
         clearResults();
         mSearchResults.addAll(data);
         notifyDataSetChanged();
-        return mSearchResults.size();
-    }
-
-    /**
-     * Notifies the adapter that all the unsorted results are loaded and now the ladapter can
-     * proceed with ranking the results.
-     */
-    @MainThread
-    public void notifyResultsLoaded() {
-        mSearchResultsLoaded = true;
-        // static ranking is skipped only if asyc ranking is already succeeded.
-        if (mAsyncRankingState != SUCCEEDED) {
-            doStaticRanking();
-        }
-        if (canUpdateSearchResults()) {
-            updateSearchResults();
-        }
     }
 
     public void clearResults() {
         mSearchResults.clear();
-        mStaticallyRankedSearchResults.clear();
-        mResultsMap.clear();
         notifyDataSetChanged();
     }
 
-    @VisibleForTesting
     public List<SearchResult> getSearchResults() {
         return mSearchResults;
     }
 
-    @MainThread
-    public void initializeSearch(String query) {
-        clearResults();
-        mSearchResultsLoaded = false;
-        mSearchResultsUpdated = false;
-        if (mSearchFeatureProvider.isSmartSearchRankingEnabled(mContext)) {
-            mAsyncRankingState = PENDING_RESULTS;
-            mSearchFeatureProvider.cancelPendingSearchQuery(mContext);
-            final Handler handler = getHandler();
-            final long timeoutMs = mSearchFeatureProvider.smartSearchRankingTimeoutMs(mContext);
-            handler.sendMessageDelayed(
-                    handler.obtainMessage(MSG_RANKING_TIMED_OUT), timeoutMs);
-            mSearchFeatureProvider.querySearchResults(mContext, query, this);
-        } else {
-            mAsyncRankingState = DISABLED;
-        }
-    }
-
-    @AsyncRankingState int getAsyncRankingState() {
-        return mAsyncRankingState;
-    }
-
-    /**
-     * Merge the results from each of the loaders into one list for the adapter.
-     * Prioritizes results from the local database over installed apps.
-     */
-    private void doStaticRanking() {
-        List<? extends SearchResult> databaseResults =
-                getSortedLoadedResults(DB_RESULTS_LOADER_KEY);
-        List<? extends SearchResult> installedAppResults =
-                getSortedLoadedResults(APP_RESULTS_LOADER_KEY);
-        List<? extends SearchResult> accessibilityResults =
-                getSortedLoadedResults(ACCESSIBILITY_LOADER_KEY);
-        List<? extends SearchResult> inputDeviceResults =
-                getSortedLoadedResults(INPUT_DEVICE_LOADER_KEY);
-
-        int dbSize = databaseResults.size();
-        int appSize = installedAppResults.size();
-        int a11ySize = accessibilityResults.size();
-        int inputDeviceSize = inputDeviceResults.size();
-        int dbIndex = 0;
-        int appIndex = 0;
-        int a11yIndex = 0;
-        int inputDeviceIndex = 0;
-        int rank = SearchResult.TOP_RANK;
-
-        // TODO: We need a helper method to do k-way merge.
-        mStaticallyRankedSearchResults.clear();
-        while (rank <= SearchResult.BOTTOM_RANK) {
-            while ((dbIndex < dbSize) && (databaseResults.get(dbIndex).rank == rank)) {
-                mStaticallyRankedSearchResults.add(databaseResults.get(dbIndex++));
-            }
-            while ((appIndex < appSize) && (installedAppResults.get(appIndex).rank == rank)) {
-                mStaticallyRankedSearchResults.add(installedAppResults.get(appIndex++));
-            }
-            while ((a11yIndex < a11ySize) && (accessibilityResults.get(a11yIndex).rank == rank)) {
-                mStaticallyRankedSearchResults.add(accessibilityResults.get(a11yIndex++));
-            }
-            while (inputDeviceIndex < inputDeviceSize
-                    && inputDeviceResults.get(inputDeviceIndex).rank == rank) {
-                mStaticallyRankedSearchResults.add(inputDeviceResults.get(inputDeviceIndex++));
-            }
-            rank++;
-        }
-
-        while (dbIndex < dbSize) {
-            mStaticallyRankedSearchResults.add(databaseResults.get(dbIndex++));
-        }
-        while (appIndex < appSize) {
-            mStaticallyRankedSearchResults.add(installedAppResults.get(appIndex++));
-        }
-        while(a11yIndex < a11ySize) {
-            mStaticallyRankedSearchResults.add(accessibilityResults.get(a11yIndex++));
-        }
-        while (inputDeviceIndex < inputDeviceSize) {
-            mStaticallyRankedSearchResults.add(inputDeviceResults.get(inputDeviceIndex++));
-        }
-    }
-
-    private void updateSearchResults() {
-        switch (mAsyncRankingState) {
-            case PENDING_RESULTS:
-                break;
-            case DISABLED:
-            case FAILED:
-            case TIMED_OUT:
-                // When DISABLED or FAILED or TIMED_OUT, we use static ranking results.
-                postSearchResults(mStaticallyRankedSearchResults, false);
-                break;
-            case SUCCEEDED:
-                postSearchResults(doAsyncRanking(), true);
-                break;
-        }
-    }
-
-    private boolean canUpdateSearchResults() {
-        // Results are not updated yet and db results are loaded and we are not waiting on async
-        // ranking scores.
-        return !mSearchResultsUpdated
-                && mSearchResultsLoaded
-                && mAsyncRankingState != PENDING_RESULTS;
-    }
-
-    @VisibleForTesting
-    List<SearchResult> doAsyncRanking() {
-        Set<? extends SearchResult> databaseResults =
-                getUnsortedLoadedResults(DB_RESULTS_LOADER_KEY);
-        List<? extends SearchResult> installedAppResults =
-                getSortedLoadedResults(APP_RESULTS_LOADER_KEY);
-        List<? extends SearchResult> accessibilityResults =
-                getSortedLoadedResults(ACCESSIBILITY_LOADER_KEY);
-        List<? extends SearchResult> inputDeviceResults =
-                getSortedLoadedResults(INPUT_DEVICE_LOADER_KEY);
-        int dbSize = databaseResults.size();
-        int appSize = installedAppResults.size();
-        int a11ySize = accessibilityResults.size();
-        int inputDeviceSize = inputDeviceResults.size();
-
-        final List<SearchResult> asyncRankingResults = new ArrayList<>(
-                dbSize + appSize + a11ySize + inputDeviceSize);
-        TreeSet<SearchResult> dbResultsSortedByScores = new TreeSet<>(
-                new Comparator<SearchResult>() {
-                    @Override
-                    public int compare(SearchResult o1, SearchResult o2) {
-                        float score1 = getRankingScoreByStableId(o1.stableId);
-                        float score2 = getRankingScoreByStableId(o2.stableId);
-                        if (score1 > score2) {
-                            return -1;
-                        } else if (score1 == score2) {
-                            return 0;
-                        } else {
-                            return 1;
-                        }
-                    }
-                });
-        dbResultsSortedByScores.addAll(databaseResults);
-        asyncRankingResults.addAll(dbResultsSortedByScores);
-        // Other results are not ranked by async ranking and appended at the end of the list.
-        asyncRankingResults.addAll(installedAppResults);
-        asyncRankingResults.addAll(accessibilityResults);
-        asyncRankingResults.addAll(inputDeviceResults);
-        return asyncRankingResults;
-    }
-
-    @VisibleForTesting
-    Set<? extends SearchResult> getUnsortedLoadedResults(String loaderKey) {
-        return mResultsMap.containsKey(loaderKey) ? mResultsMap.get(loaderKey) : new HashSet<>();
-    }
-
-    @VisibleForTesting
-    List<? extends SearchResult> getSortedLoadedResults(String loaderKey) {
-        List<? extends SearchResult> sortedLoadedResults =
-                new ArrayList<>(getUnsortedLoadedResults(loaderKey));
-        Collections.sort(sortedLoadedResults);
-        return sortedLoadedResults;
-    }
-
-    /**
-     * Looks up ranking score for stableId
-     * @param stableId String of stableId
-     * @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 getRankingScoreByStableId(int stableId) {
-        for (Pair<String, Float> rankingScore : mSearchRankingScores) {
-            if (Integer.toString(stableId).compareTo(rankingScore.first) == 0) {
-                return rankingScore.second;
-            }
-        }
-        // If stableId not found in the list, we assign the minimum score so it will appear at
-        // the end of the list.
-        Log.w(TAG, "stableId " + stableId + " was not in the ranking scores.");
-        return -Float.MAX_VALUE;
-    }
-
-    @VisibleForTesting
-    Handler getHandler() {
-        if (mHandler == null) {
-            mHandler = new Handler(Looper.getMainLooper()) {
-                @Override
-                public void handleMessage(Message msg) {
-                    if (msg.what == MSG_RANKING_TIMED_OUT) {
-                        mSearchFeatureProvider.cancelPendingSearchQuery(mContext);
-                        if (mAsyncRankingState == PENDING_RESULTS) {
-                            mAsyncRankingState = TIMED_OUT;
-                            if (canUpdateSearchResults()) {
-                                updateSearchResults();
-                            }
-                        } else {
-                            Log.w(TAG, "Ranking scores timed out in invalid state: " +
-                                    mAsyncRankingState);
-                        }
-                    }
-                }
-            };
-        }
-        return mHandler;
-    }
-
-    @VisibleForTesting
-    public void postSearchResults(List<SearchResult> newSearchResults, boolean detectMoves) {
+    public void postSearchResults(List<? extends SearchResult> newSearchResults) {
         final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
-                new SearchResultDiffCallback(mSearchResults, newSearchResults), detectMoves);
+                new SearchResultDiffCallback(mSearchResults, newSearchResults));
         mSearchResults.clear();
         mSearchResults.addAll(newSearchResults);
         diffResult.dispatchUpdatesTo(this);
         mFragment.onSearchResultsDisplayed(mSearchResults.size());
-        mSearchResultsUpdated = true;
     }
 }
diff --git a/src/com/android/settings/EncryptionAndCredential.java b/src/com/android/settings/security/EncryptionAndCredential.java
similarity index 93%
rename from src/com/android/settings/EncryptionAndCredential.java
rename to src/com/android/settings/security/EncryptionAndCredential.java
index 4892f7e..0972e3e 100644
--- a/src/com/android/settings/EncryptionAndCredential.java
+++ b/src/com/android/settings/security/EncryptionAndCredential.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2007 The Android Open Source Project
+ * 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.
@@ -14,13 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.settings;
+package com.android.settings.security;
 
-import android.app.Activity;
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.content.Intent;
-import android.os.Bundle;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.SearchIndexableResource;
@@ -30,11 +28,12 @@
 
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.widget.LockPatternUtils;
+import com.android.settings.R;
 import com.android.settings.dashboard.DashboardFragment;
 import com.android.settings.search.BaseSearchIndexProvider;
-import com.android.settings.search.Indexable;
 import com.android.settingslib.RestrictedLockUtils;
 import com.android.settingslib.RestrictedPreference;
+import com.android.settingslib.core.AbstractPreferenceController;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -43,7 +42,7 @@
  * Encryption and Credential settings.
  * TODO: Extends this from {@link DashboardFragment} instead
  */
-public class EncryptionAndCredential extends SettingsPreferenceFragment implements Indexable {
+public class EncryptionAndCredential extends DashboardFragment {
 
     private static final String TAG = "EncryptionAndCredential";
 
@@ -69,12 +68,19 @@
     }
 
     @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
+    protected String getLogTag() {
+        return TAG;
+    }
 
-        final Activity activity = getActivity();
+    @Override
+    protected List<AbstractPreferenceController> getPreferenceControllers(Context context) {
+        mUm = (UserManager) context.getSystemService(Context.USER_SERVICE);
+        return null;
+    }
 
-        mUm = UserManager.get(activity);
+    @Override
+    protected int getPreferenceScreenResId() {
+        return 0;
     }
 
     /**
diff --git a/src/com/android/settings/wifi/WifiConfigController.java b/src/com/android/settings/wifi/WifiConfigController.java
index 8ee340a..5a10ebf 100644
--- a/src/com/android/settings/wifi/WifiConfigController.java
+++ b/src/com/android/settings/wifi/WifiConfigController.java
@@ -61,6 +61,7 @@
 import com.android.settings.ProxySelector;
 import com.android.settings.R;
 import com.android.settingslib.Utils;
+import com.android.settingslib.utils.ThreadUtils;
 import com.android.settingslib.wifi.AccessPoint;
 
 import java.net.Inet4Address;
@@ -116,8 +117,6 @@
     /* Full list of phase2 methods */
     private final ArrayAdapter<String> mPhase2FullAdapter;
 
-    private final Handler mTextViewChangedHandler;
-
     // e.g. AccessPoint.SECURITY_NONE
     private int mAccessPointSecurity;
     private TextView mPasswordView;
@@ -175,7 +174,6 @@
                 accessPoint.getSecurity();
         mMode = mode;
 
-        mTextViewChangedHandler = new Handler();
         mContext = mConfigUi.getContext();
         final Resources res = mContext.getResources();
 
@@ -1258,12 +1256,10 @@
 
     @Override
     public void afterTextChanged(Editable s) {
-        mTextViewChangedHandler.post(new Runnable() {
-                public void run() {
-                    showWarningMessagesIfAppropriate();
-                    enableSubmitIfAppropriate();
-                }
-            });
+        ThreadUtils.postOnMainThread(() -> {
+            showWarningMessagesIfAppropriate();
+            enableSubmitIfAppropriate();
+        });
     }
 
     @Override
diff --git a/src/com/android/settings/wifi/WriteWifiConfigToNfcDialog.java b/src/com/android/settings/wifi/WriteWifiConfigToNfcDialog.java
index dfed801..dc98e21 100644
--- a/src/com/android/settings/wifi/WriteWifiConfigToNfcDialog.java
+++ b/src/com/android/settings/wifi/WriteWifiConfigToNfcDialog.java
@@ -63,7 +63,6 @@
     private View mView;
     private Button mSubmitButton;
     private Button mCancelButton;
-    private Handler mOnTextChangedHandler;
     private TextView mPasswordView;
     private TextView mLabelView;
     private CheckBox mPasswordCheckBox;
@@ -79,7 +78,6 @@
         mContext = context;
         mWakeLock = ((PowerManager) context.getSystemService(Context.POWER_SERVICE))
                 .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "WriteWifiConfigToNfcDialog:wakeLock");
-        mOnTextChangedHandler = new Handler();
         mSecurity = security;
         mWifiManager = wifiManager;
     }
@@ -90,7 +88,6 @@
         mContext = context;
         mWakeLock = ((PowerManager) context.getSystemService(Context.POWER_SERVICE))
                 .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "WriteWifiConfigToNfcDialog:wakeLock");
-        mOnTextChangedHandler = new Handler();
         mSecurity = savedState.getInt(SECURITY);
         mWifiManager = wifiManager;
     }
@@ -226,12 +223,7 @@
 
     @Override
     public void onTextChanged(CharSequence s, int start, int before, int count) {
-        mOnTextChangedHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                enableSubmitIfAppropriate();
-            }
-        });
+        enableSubmitIfAppropriate();
     }
 
     private void enableSubmitIfAppropriate() {
diff --git a/tests/anomaly-tester/res/values/strings.xml b/tests/anomaly-tester/res/values/strings.xml
index 99de34e..da56356 100644
--- a/tests/anomaly-tester/res/values/strings.xml
+++ b/tests/anomaly-tester/res/values/strings.xml
@@ -15,5 +15,5 @@
 -->
 
 <resources>
-    <string name="app_name">AnomalyTester</string>
+    <string name="app_name" translatable="false">AnomalyTester</string>
 </resources>
diff --git a/tests/robotests/README.md b/tests/robotests/README.md
new file mode 100644
index 0000000..648f1af
--- /dev/null
+++ b/tests/robotests/README.md
@@ -0,0 +1,24 @@
+# Running Settings Robolectric tests
+
+
+## The full suite
+```
+$ croot
+$ make RunSettingsRoboTests
+```
+
+## Running a single test class
+
+```
+$ croot
+$ make RunSettingsRoboTests ROBOTEST_FILTER=<ClassName>
+```
+
+For example:
+
+```
+make RunSettingsRoboTests ROBOTEST_FILTER=CodeInspectionTest
+```
+
+You can also use partial class name in ROBOTEST_FILTER. If the partial class name matches
+multiple file names, all of them will be executed.
diff --git a/tests/robotests/src/com/android/settings/core/InstrumentedPreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/core/InstrumentedPreferenceFragmentTest.java
index ab7228c..30d60cc 100644
--- a/tests/robotests/src/com/android/settings/core/InstrumentedPreferenceFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/core/InstrumentedPreferenceFragmentTest.java
@@ -74,17 +74,6 @@
     }
 
     @Test
-    public void onCreatePreferences_preferenceScreenTitleFeatureOff_shouldNotAddPreference() {
-        SettingsShadowSystemProperties.set(
-                FeatureFlagUtils.FFLAG_PREFIX + mFragment.FEATURE_FLAG_USE_PREFERENCE_SCREEN_TITLE,
-                "false");
-
-        mFragment.onCreatePreferences(Bundle.EMPTY, null /* rootKey */);
-
-        verify(mFragment, never()).addPreferencesFromResource(anyInt());
-    }
-
-    @Test
     public void onCreatePreferences_noPreferenceScreenResId_shouldNotAddPreference() {
         SettingsShadowSystemProperties.set(
                 FeatureFlagUtils.FFLAG_PREFIX + mFragment.FEATURE_FLAG_USE_PREFERENCE_SCREEN_TITLE,
diff --git a/tests/robotests/src/com/android/settings/fingerprint/FingerprintEnrollEnrollingTest.java b/tests/robotests/src/com/android/settings/fingerprint/FingerprintEnrollEnrollingTest.java
new file mode 100644
index 0000000..ace535e
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fingerprint/FingerprintEnrollEnrollingTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.fingerprint;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.verify;
+import static org.robolectric.RuntimeEnvironment.application;
+
+import android.content.Intent;
+import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.FingerprintManager.EnrollmentCallback;
+import android.media.AudioAttributes;
+import android.os.CancellationSignal;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.widget.TextView;
+
+import com.android.settings.R;
+import com.android.settings.TestConfig;
+import com.android.settings.password.ChooseLockSettingsHelper;
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+import com.android.settings.testutils.shadow.ShadowUtils;
+import com.android.settings.testutils.shadow.ShadowVibrator;
+import com.android.settings.wrapper.FingerprintManagerWrapper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+import java.util.concurrent.TimeUnit;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(
+        manifest = TestConfig.MANIFEST_PATH,
+        sdk = Config.NEWEST_SDK,
+        shadows = {
+                ShadowUtils.class,
+                ShadowVibrator.class
+        })
+public class FingerprintEnrollEnrollingTest {
+
+    @Mock
+    private FingerprintManagerWrapper mFingerprintManager;
+
+    private FingerprintEnrollEnrolling mActivity;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        ShadowUtils.setFingerprintManager(mFingerprintManager);
+        ShadowVibrator.addToServiceMap();
+
+        mActivity = Robolectric.buildActivity(
+                FingerprintEnrollEnrolling.class,
+                new Intent()
+                        // Set the challenge token so the confirm screen will not be shown
+                        .putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, new byte[0]))
+                .setup().get();
+    }
+
+    @After
+    public void tearDown() {
+        ShadowUtils.reset();
+        ShadowVibrator.reset();
+    }
+
+    @Test
+    public void fingerprintEnrollHelp_shouldShowHelpTextAndVibrate() {
+        EnrollmentCallback enrollmentCallback = verifyAndCaptureEnrollmentCallback();
+
+        enrollmentCallback.onEnrollmentProgress(123);
+        enrollmentCallback.onEnrollmentHelp(
+                FingerprintManager.FINGERPRINT_ERROR_UNABLE_TO_PROCESS,
+                "test enrollment help");
+
+        TextView errorText = mActivity.findViewById(R.id.error_text);
+        assertThat(errorText.getText()).isEqualTo("test enrollment help");
+
+        Robolectric.getForegroundThreadScheduler().advanceBy(2, TimeUnit.MILLISECONDS);
+
+
+        ShadowVibrator shadowVibrator =
+                Shadow.extract(application.getSystemService(Vibrator.class));
+        verify(shadowVibrator.delegate).vibrate(
+                anyInt(),
+                nullable(String.class),
+                any(VibrationEffect.class),
+                nullable(AudioAttributes.class));
+    }
+
+    private EnrollmentCallback verifyAndCaptureEnrollmentCallback() {
+        ArgumentCaptor<EnrollmentCallback> callbackCaptor =
+                ArgumentCaptor.forClass(EnrollmentCallback.class);
+        verify(mFingerprintManager).enroll(
+                any(byte[].class),
+                any(CancellationSignal.class),
+                anyInt(),
+                anyInt(),
+                callbackCaptor.capture());
+
+        return callbackCaptor.getValue();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/search/AccessibilityServiceResultLoaderTest.java b/tests/robotests/src/com/android/settings/search/AccessibilityServiceResultFutureTaskTest.java
similarity index 83%
rename from tests/robotests/src/com/android/settings/search/AccessibilityServiceResultLoaderTest.java
rename to tests/robotests/src/com/android/settings/search/AccessibilityServiceResultFutureTaskTest.java
index 4896dc4..0e4abe1 100644
--- a/tests/robotests/src/com/android/settings/search/AccessibilityServiceResultLoaderTest.java
+++ b/tests/robotests/src/com/android/settings/search/AccessibilityServiceResultFutureTaskTest.java
@@ -17,6 +17,7 @@
 package com.android.settings.search;
 
 import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -46,7 +47,7 @@
 
 @RunWith(SettingsRobolectricTestRunner.class)
 @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class AccessibilityServiceResultLoaderTest {
+public class AccessibilityServiceResultFutureTaskTest {
 
     private static final String QUERY = "test_query";
 
@@ -59,7 +60,7 @@
     @Mock
     private SiteMapManager mSiteMapManager;
 
-    private AccessibilityServiceResultLoader mLoader;
+    private AccessibilityServiceResultLoader.AccessibilityServiceResultCallable mCallable;
 
     @Before
     public void setUp() {
@@ -68,19 +69,20 @@
                 .thenReturn(mAccessibilityManager);
         when(mContext.getPackageManager()).thenReturn(mPackageManager);
 
-        mLoader = new AccessibilityServiceResultLoader(mContext, QUERY, mSiteMapManager);
+        mCallable = new AccessibilityServiceResultLoader.AccessibilityServiceResultCallable(
+                mContext, QUERY, mSiteMapManager);
     }
 
     @Test
-    public void query_noService_shouldNotReturnAnything() {
-        assertThat(mLoader.loadInBackground()).isEmpty();
+    public void query_noService_shouldNotReturnAnything() throws Exception {
+        assertThat(mCallable.call()).isEmpty();
     }
 
     @Test
-    public void query_hasServiceMatchingTitle_shouldReturnResult() {
+    public void query_hasServiceMatchingTitle_shouldReturnResult() throws Exception {
         addFakeAccessibilityService();
 
-        List<? extends SearchResult> results = new ArrayList<>(mLoader.loadInBackground());
+        List<? extends SearchResult> results = mCallable.call();
         assertThat(results).hasSize(1);
 
         SearchResult result = results.get(0);
@@ -88,13 +90,14 @@
     }
 
     @Test
-    public void query_serviceDoesNotMatchTitle_shouldReturnResult() {
+    public void query_serviceDoesNotMatchTitle_shouldReturnResult() throws Exception {
         addFakeAccessibilityService();
 
-        mLoader = new AccessibilityServiceResultLoader(mContext,
+        mCallable = new AccessibilityServiceResultLoader.AccessibilityServiceResultCallable(
+                mContext,
                 QUERY + "no_match", mSiteMapManager);
 
-        assertThat(mLoader.loadInBackground()).isEmpty();
+        assertThat(mCallable.call()).isEmpty();
     }
 
     private void addFakeAccessibilityService() {
diff --git a/tests/robotests/src/com/android/settings/search/InputDeviceResultLoaderTest.java b/tests/robotests/src/com/android/settings/search/InputDeviceResultFutureTaskTest.java
similarity index 87%
rename from tests/robotests/src/com/android/settings/search/InputDeviceResultLoaderTest.java
rename to tests/robotests/src/com/android/settings/search/InputDeviceResultFutureTaskTest.java
index a955af1..e31b3d7 100644
--- a/tests/robotests/src/com/android/settings/search/InputDeviceResultLoaderTest.java
+++ b/tests/robotests/src/com/android/settings/search/InputDeviceResultFutureTaskTest.java
@@ -17,9 +17,11 @@
 package com.android.settings.search;
 
 import static android.content.Context.INPUT_METHOD_SERVICE;
+
 import static com.android.settings.search.InputDeviceResultLoader.PHYSICAL_KEYBOARD_FRAGMENT;
 import static com.android.settings.search.InputDeviceResultLoader.VIRTUAL_KEYBOARD_FRAGMENT;
 import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verifyZeroInteractions;
@@ -58,7 +60,7 @@
         shadows = {
                 ShadowInputDevice.class
         })
-public class InputDeviceResultLoaderTest {
+public class InputDeviceResultFutureTaskTest {
 
     private static final String QUERY = "test_query";
     private static final List<String> PHYSICAL_KEYBOARD_BREADCRUMB;
@@ -84,7 +86,7 @@
     @Mock
     private PackageManager mPackageManager;
 
-    private InputDeviceResultLoader mLoader;
+    private InputDeviceResultLoader.InputDeviceResultCallable mCallable;
 
     @Before
     public void setUp() {
@@ -99,7 +101,8 @@
         when(mContext.getString(anyInt()))
                 .thenAnswer(invocation -> RuntimeEnvironment.application.getString(
                         (Integer) invocation.getArguments()[0]));
-        mLoader = new InputDeviceResultLoader(mContext, QUERY, mSiteMapManager);
+        mCallable = new InputDeviceResultLoader.InputDeviceResultCallable(mContext, QUERY,
+                mSiteMapManager);
     }
 
     @After
@@ -108,18 +111,19 @@
     }
 
     @Test
-    public void query_noKeyboard_shouldNotReturnAnything() {
-        assertThat(mLoader.loadInBackground()).isEmpty();
+    public void query_noKeyboard_shouldNotReturnAnything() throws Exception {
+
+        assertThat(mCallable.call()).isEmpty();
     }
 
     @Test
-    public void query_hasPhysicalKeyboard_match() {
+    public void query_hasPhysicalKeyboard_match() throws Exception {
         addPhysicalKeyboard(QUERY);
         when(mSiteMapManager.buildBreadCrumb(mContext, PHYSICAL_KEYBOARD_FRAGMENT,
                 RuntimeEnvironment.application.getString(R.string.physical_keyboard_title)))
                 .thenReturn(PHYSICAL_KEYBOARD_BREADCRUMB);
 
-        final List<SearchResult> results = new ArrayList<>(mLoader.loadInBackground());
+        final List<? extends SearchResult> results = mCallable.call();
 
         assertThat(results).hasSize(1);
         assertThat(results.get(0).title).isEqualTo(QUERY);
@@ -128,13 +132,13 @@
     }
 
     @Test
-    public void query_hasVirtualKeyboard_match() {
+    public void query_hasVirtualKeyboard_match() throws Exception {
         addVirtualKeyboard(QUERY);
         when(mSiteMapManager.buildBreadCrumb(mContext, VIRTUAL_KEYBOARD_FRAGMENT,
                 RuntimeEnvironment.application.getString(R.string.add_virtual_keyboard)))
                 .thenReturn(VIRTUAL_KEYBOARD_BREADCRUMB);
 
-        final List<SearchResult> results = new ArrayList<>(mLoader.loadInBackground());
+        final List<? extends SearchResult> results = mCallable.call();
         assertThat(results).hasSize(1);
         assertThat(results.get(0).title).isEqualTo(QUERY);
         assertThat(results.get(0).breadcrumbs)
@@ -142,11 +146,11 @@
     }
 
     @Test
-    public void query_hasPhysicalVirtualKeyboard_doNotMatch() {
+    public void query_hasPhysicalVirtualKeyboard_doNotMatch() throws Exception {
         addPhysicalKeyboard("abc");
         addVirtualKeyboard("def");
 
-        assertThat(mLoader.loadInBackground()).isEmpty();
+        assertThat(mCallable.call()).isEmpty();
         verifyZeroInteractions(mSiteMapManager);
     }
 
@@ -170,4 +174,4 @@
         when(mImm.getInputMethodList()).thenReturn(imis);
     }
 
-}
+}
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/search/InstalledAppResultLoaderTest.java b/tests/robotests/src/com/android/settings/search/InstalledAppResultLoaderTest.java
index f1a25a1..0e84dc7 100644
--- a/tests/robotests/src/com/android/settings/search/InstalledAppResultLoaderTest.java
+++ b/tests/robotests/src/com/android/settings/search/InstalledAppResultLoaderTest.java
@@ -19,7 +19,9 @@
 
 import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
 import static android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
+
 import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.anyList;
@@ -53,8 +55,6 @@
 import org.mockito.Answers;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
 import org.robolectric.annotation.Config;
 
 import java.util.ArrayList;
@@ -77,7 +77,7 @@
     @Mock
     private SiteMapManager mSiteMapManager;
 
-    private InstalledAppResultLoader mLoader;
+    private InstalledAppResultLoader.InstalledAppResultCallable mCallable;
 
     @Before
     public void setUp() {
@@ -109,49 +109,50 @@
     }
 
     @Test
-    public void query_noMatchingQuery_shouldReturnEmptyResult() {
+    public void query_noMatchingQuery_shouldReturnEmptyResult() throws Exception {
         final String query = "abc";
 
-        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+        mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
+                mPackageManagerWrapper, query,
                 mSiteMapManager);
 
-        assertThat(mLoader.loadInBackground()).isEmpty();
+        assertThat(mCallable.call()).isEmpty();
     }
 
     @Test
-    public void query_matchingQuery_shouldReturnNonSystemApps() {
+    public void query_matchingQuery_shouldReturnNonSystemApps() throws Exception {
         final String query = "app";
 
-        mLoader = spy(new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+        mCallable = spy(new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
+                mPackageManagerWrapper, query,
                 mSiteMapManager));
-        when(mLoader.getContext()).thenReturn(mContext);
         when(mSiteMapManager.buildBreadCrumb(eq(mContext), anyString(), anyString()))
                 .thenReturn(Arrays.asList(new String[]{"123"}));
 
-        assertThat(mLoader.loadInBackground().size()).isEqualTo(3);
+        assertThat(mCallable.call()).hasSize(3);
         verify(mSiteMapManager)
                 .buildBreadCrumb(eq(mContext), anyString(), anyString());
     }
 
     @Test
-    public void query_matchingQuery_shouldReturnSystemAppUpdates() {
+    public void query_matchingQuery_shouldReturnSystemAppUpdates() throws Exception {
         when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
                 .thenReturn(Arrays.asList(
                         ApplicationTestUtils.buildInfo(0 /* uid */, "app1", FLAG_UPDATED_SYSTEM_APP,
                                 0 /* targetSdkVersion */)));
         final String query = "app";
 
-        mLoader = spy(new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+        mCallable = spy(new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
+                mPackageManagerWrapper, query,
                 mSiteMapManager));
-        when(mLoader.getContext()).thenReturn(mContext);
 
-        assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
+        assertThat(mCallable.call()).hasSize(1);
         verify(mSiteMapManager)
                 .buildBreadCrumb(eq(mContext), anyString(), anyString());
     }
 
     @Test
-    public void query_matchingQuery_shouldReturnSystemAppIfLaunchable() {
+    public void query_matchingQuery_shouldReturnSystemAppIfLaunchable() throws Exception {
         when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
                 .thenReturn(Arrays.asList(
                         ApplicationTestUtils.buildInfo(0 /* uid */, "app1", FLAG_SYSTEM,
@@ -164,14 +165,15 @@
 
         final String query = "app";
 
-        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+        mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
+                mPackageManagerWrapper, query,
                 mSiteMapManager);
 
-        assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
+        assertThat(mCallable.call()).hasSize(1);
     }
 
     @Test
-    public void query_matchingQuery_shouldReturnSystemAppIfHomeApp() {
+    public void query_matchingQuery_shouldReturnSystemAppIfHomeApp() throws Exception {
         when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
                 .thenReturn(Arrays.asList(
                         ApplicationTestUtils.buildInfo(0 /* uid */, "app1", FLAG_SYSTEM,
@@ -180,28 +182,26 @@
                 any(Intent.class), anyInt(), anyInt()))
                 .thenReturn(null);
 
-        when(mPackageManagerWrapper.getHomeActivities(anyList())).thenAnswer(new Answer<Object>() {
-            @Override
-            public Object answer(InvocationOnMock invocation) throws Throwable {
-                final List<ResolveInfo> list = (List<ResolveInfo>) invocation.getArguments()[0];
-                final ResolveInfo info = new ResolveInfo();
-                info.activityInfo = new ActivityInfo();
-                info.activityInfo.packageName = "app1";
-                list.add(info);
-                return null;
-            }
+        when(mPackageManagerWrapper.getHomeActivities(anyList())).thenAnswer(invocation -> {
+            final List<ResolveInfo> list = (List<ResolveInfo>) invocation.getArguments()[0];
+            final ResolveInfo info = new ResolveInfo();
+            info.activityInfo = new ActivityInfo();
+            info.activityInfo.packageName = "app1";
+            list.add(info);
+            return null;
         });
 
         final String query = "app";
 
-        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+        mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
+                mPackageManagerWrapper, query,
                 mSiteMapManager);
 
-        assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
+        assertThat(mCallable.call()).hasSize(1);
     }
 
     @Test
-    public void query_matchingQuery_shouldNotReturnSystemAppIfNotLaunchable() {
+    public void query_matchingQuery_shouldNotReturnSystemAppIfNotLaunchable() throws Exception {
         when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
                 .thenReturn(Arrays.asList(
                         ApplicationTestUtils.buildInfo(0 /* uid */, "app1", FLAG_SYSTEM,
@@ -212,21 +212,23 @@
 
         final String query = "app";
 
-        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+        mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
+                mPackageManagerWrapper, query,
                 mSiteMapManager);
 
-        assertThat(mLoader.loadInBackground()).isEmpty();
+        assertThat(mCallable.call()).isEmpty();
         verify(mSiteMapManager, never())
                 .buildBreadCrumb(eq(mContext), anyString(), anyString());
     }
 
     @Test
-    public void query_matchingQuery_multipleResults() {
+    public void query_matchingQuery_multipleResults() throws Exception {
         final String query = "app";
 
-        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+        mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
+                mPackageManagerWrapper, query,
                 mSiteMapManager);
-        final Set<? extends SearchResult> results = mLoader.loadInBackground();
+        final List<? extends SearchResult> results = mCallable.call();
 
         Set<CharSequence> expectedTitles = new HashSet<>(Arrays.asList("app4", "app", "appBuffer"));
         Set<CharSequence> actualTitles = new HashSet<>();
@@ -237,161 +239,172 @@
     }
 
     @Test
-    public void query_normalWord_MatchPrefix() {
+    public void query_normalWord_MatchPrefix() throws Exception {
         final String query = "ba";
         final String packageName = "Bananas";
         when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
                 .thenReturn(Arrays.asList(
                         ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
                                 0 /* targetSdkVersion */)));
-        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+        mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
+                mPackageManagerWrapper, query,
                 mSiteMapManager);
 
-        assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
+        assertThat(mCallable.call()).hasSize(1);
     }
 
     @Test
-    public void query_CapitalCase_DoestMatchSecondWord() {
+    public void query_CapitalCase_DoestMatchSecondWord() throws Exception {
         final String query = "Apples";
         final String packageName = "BananasApples";
         when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
                 .thenReturn(Arrays.asList(
                         ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
                                 0 /* targetSdkVersion */)));
-        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+        mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
+                mPackageManagerWrapper, query,
                 mSiteMapManager);
 
-        assertThat(mLoader.loadInBackground().size()).isEqualTo(0);
+        assertThat(mCallable.call()).isEmpty();
     }
 
     @Test
-    public void query_TwoWords_MatchesFirstWord() {
+    public void query_TwoWords_MatchesFirstWord() throws Exception {
         final String query = "Banana";
         final String packageName = "Bananas Apples";
         when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
                 .thenReturn(Arrays.asList(
                         ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
                                 0 /* targetSdkVersion */)));
-        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+        mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
+                mPackageManagerWrapper, query,
                 mSiteMapManager);
 
-        assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
+        assertThat(mCallable.call()).hasSize(1);
     }
 
     @Test
-    public void query_TwoWords_MatchesSecondWord() {
+    public void query_TwoWords_MatchesSecondWord() throws Exception {
         final String query = "Apple";
         final String packageName = "Bananas Apples";
         when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
                 .thenReturn(Arrays.asList(
                         ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
                                 0 /* targetSdkVersion */)));
-        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+        mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
+                mPackageManagerWrapper, query,
                 mSiteMapManager);
 
-        assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
+        assertThat(mCallable.call()).hasSize(1);
     }
 
     @Test
-    public void query_ThreeWords_MatchesThirdWord() {
+    public void query_ThreeWords_MatchesThirdWord() throws Exception {
         final String query = "Pear";
         final String packageName = "Bananas Apples Pears";
         when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
                 .thenReturn(Arrays.asList(
                         ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
                                 0 /* targetSdkVersion */)));
-        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+        mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
+                mPackageManagerWrapper, query,
                 mSiteMapManager);
 
-        assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
+        assertThat(mCallable.call()).hasSize(1);
     }
 
     @Test
-    public void query_DoubleSpacedWords_MatchesSecondWord() {
+    public void query_DoubleSpacedWords_MatchesSecondWord() throws Exception {
         final String query = "Apple";
         final String packageName = "Bananas  Apples";
         when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
                 .thenReturn(Arrays.asList(
                         ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
                                 0 /* targetSdkVersion */)));
-        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+        mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
+                mPackageManagerWrapper, query,
                 mSiteMapManager);
 
-        assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
+        assertThat(mCallable.call()).hasSize(1);
     }
 
     @Test
-    public void query_SpecialChar_MatchesSecondWord() {
+    public void query_SpecialChar_MatchesSecondWord() throws Exception {
         final String query = "Apple";
         final String packageName = "Bananas & Apples";
         when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
                 .thenReturn(Arrays.asList(
                         ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
                                 0 /* targetSdkVersion */)));
-        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+        mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
+                mPackageManagerWrapper, query,
                 mSiteMapManager);
 
-        assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
+        assertThat(mCallable.call()).hasSize(1);
     }
 
     @Test
-    public void query_TabSeparated_MatchesSecondWord() {
+    public void query_TabSeparated_MatchesSecondWord() throws Exception {
         final String query = "Apple";
         final String packageName = "Bananas\tApples";
         when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
                 .thenReturn(Arrays.asList(
                         ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
                                 0 /* targetSdkVersion */)));
-        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+        mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
+                mPackageManagerWrapper, query,
                 mSiteMapManager);
 
-        assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
+        assertThat(mCallable.call()).hasSize(1);
     }
 
     @Test
-    public void query_LeadingNumber_MatchesWord() {
+    public void query_LeadingNumber_MatchesWord() throws Exception {
         final String query = "4";
         final String packageName = "4Bananas";
         when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
                 .thenReturn(Arrays.asList(
                         ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
                                 0 /* targetSdkVersion */)));
-        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+        mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
+                mPackageManagerWrapper, query,
                 mSiteMapManager);
 
-        assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
+        assertThat(mCallable.call()).hasSize(1);
     }
 
     @Test
-    public void query_FirstWordPrefixOfQuery_NoMatch() {
+    public void query_FirstWordPrefixOfQuery_NoMatch() throws Exception {
         final String query = "Bananass";
         final String packageName = "Bananas Apples";
         when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
                 .thenReturn(Arrays.asList(
                         ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
                                 0 /* targetSdkVersion */)));
-        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+        mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
+                mPackageManagerWrapper, query,
                 mSiteMapManager);
 
-        assertThat(mLoader.loadInBackground().size()).isEqualTo(0);
+        assertThat(mCallable.call()).isEmpty();
     }
 
     @Test
-    public void query_QueryLongerThanAppName_NoMatch() {
+    public void query_QueryLongerThanAppName_NoMatch() throws Exception {
         final String query = "BananasApples";
         final String packageName = "Bananas";
         when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
                 .thenReturn(Arrays.asList(
                         ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
                                 0 /* targetSdkVersion */)));
-        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+        mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
+                mPackageManagerWrapper, query,
                 mSiteMapManager);
 
-        assertThat(mLoader.loadInBackground().size()).isEqualTo(0);
+        assertThat(mCallable.call()).isEmpty();
     }
 
     @Test
-    public void query_appExistsInBothProfiles() {
+    public void query_appExistsInBothProfiles() throws Exception {
         final String query = "carrot";
         final String packageName = "carrot";
         final int user1 = 0;
@@ -414,10 +427,11 @@
                                 packageName, 0 /* flags */,
                                 0 /* targetSdkVersion */)));
 
-        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+        mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
+                mPackageManagerWrapper, query,
                 mSiteMapManager);
 
-        Set<AppSearchResult> searchResults = (Set<AppSearchResult>) mLoader.loadInBackground();
+        List<AppSearchResult> searchResults = (List<AppSearchResult>) mCallable.call();
         assertThat(searchResults).hasSize(2);
 
         Set<Integer> uidResults = searchResults.stream().map(result -> result.info.uid).collect(
diff --git a/tests/robotests/src/com/android/settings/search/MockAccessibilityLoader.java b/tests/robotests/src/com/android/settings/search/MockAccessibilityLoader.java
deleted file mode 100644
index 0a06a35..0000000
--- a/tests/robotests/src/com/android/settings/search/MockAccessibilityLoader.java
+++ /dev/null
@@ -1,39 +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.search;
-
-import android.content.Context;
-
-import java.util.HashSet;
-import java.util.Set;
-
-public class MockAccessibilityLoader extends AccessibilityServiceResultLoader {
-
-    public MockAccessibilityLoader(Context context) {
-        super(context, "test_query", null);
-    }
-
-    @Override
-    public Set<? extends SearchResult> loadInBackground() {
-        return new HashSet<>();
-    }
-
-    @Override
-    protected void onDiscardResult(Set<? extends SearchResult> result) {
-
-    }
-}
diff --git a/tests/robotests/src/com/android/settings/search/MockAppLoader.java b/tests/robotests/src/com/android/settings/search/MockAppLoader.java
deleted file mode 100644
index c68cbdf..0000000
--- a/tests/robotests/src/com/android/settings/search/MockAppLoader.java
+++ /dev/null
@@ -1,46 +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.search;
-
-import android.content.Context;
-import com.android.settings.search.InstalledAppResultLoader;
-import com.android.settings.search.SearchResult;
-
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * Mock loader to subvert the requirements of returning data while also driving the Loader
- * lifecycle.
- */
-class MockAppLoader extends InstalledAppResultLoader {
-
-    public MockAppLoader(Context context) {
-        super(context, null, "", null);
-    }
-
-    @Override
-    public Set<? extends SearchResult> loadInBackground() {
-        return new HashSet<>();
-    }
-
-    @Override
-    protected void onDiscardResult(Set<? extends SearchResult> result) {
-
-    }
-}
diff --git a/tests/robotests/src/com/android/settings/search/MockDBLoader.java b/tests/robotests/src/com/android/settings/search/MockDBLoader.java
deleted file mode 100644
index b28c1ed..0000000
--- a/tests/robotests/src/com/android/settings/search/MockDBLoader.java
+++ /dev/null
@@ -1,46 +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.search;
-
-import android.content.Context;
-import com.android.settings.search.DatabaseResultLoader;
-import com.android.settings.search.SearchResult;
-
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * Mock loader to subvert the requirements of returning data while also driving the Loader
- * lifecycle.
- */
-class MockDBLoader extends DatabaseResultLoader {
-
-    public MockDBLoader(Context context) {
-        super(context, "test", null);
-    }
-
-    @Override
-    public Set<? extends SearchResult> loadInBackground() {
-        return new HashSet<>();
-    }
-
-    @Override
-    protected void onDiscardResult(Set<? extends SearchResult> result) {
-
-    }
-}
diff --git a/tests/robotests/src/com/android/settings/search/MockInputDeviceResultLoader.java b/tests/robotests/src/com/android/settings/search/MockInputDeviceResultLoader.java
deleted file mode 100644
index 2c16b14..0000000
--- a/tests/robotests/src/com/android/settings/search/MockInputDeviceResultLoader.java
+++ /dev/null
@@ -1,38 +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.search;
-
-import android.content.Context;
-
-import java.util.HashSet;
-import java.util.Set;
-
-public class MockInputDeviceResultLoader extends InputDeviceResultLoader {
-    public MockInputDeviceResultLoader(Context context) {
-        super(context, "test_query", null);
-    }
-
-    @Override
-    public Set<? extends SearchResult> loadInBackground() {
-        return new HashSet<>();
-    }
-
-    @Override
-    protected void onDiscardResult(Set<? extends SearchResult> result) {
-
-    }
-}
diff --git a/tests/robotests/src/com/android/settings/search/MockSearchResultLoader.java b/tests/robotests/src/com/android/settings/search/MockSearchResultLoader.java
new file mode 100644
index 0000000..6af258a
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/search/MockSearchResultLoader.java
@@ -0,0 +1,29 @@
+package com.android.settings.search;
+
+import android.content.Context;
+
+import com.android.settings.search.SearchResult;
+import com.android.settings.search.SearchResultLoader;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Mock loader to subvert the requirements of returning data while also driving the Loader
+ * lifecycle.
+ */
+public class MockSearchResultLoader extends SearchResultLoader {
+
+    public MockSearchResultLoader(Context context) {
+        super(context, "test");
+    }
+
+    @Override
+    public List<? extends SearchResult> loadInBackground() {
+        return new ArrayList<>();
+    }
+
+    @Override
+    protected void onDiscardResult(List<? extends SearchResult> result) {
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java
index 050d7aa..a529b0b 100644
--- a/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java
+++ b/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java
@@ -21,7 +21,6 @@
 
 import android.app.Activity;
 import android.content.ComponentName;
-
 import com.android.settings.TestConfig;
 import com.android.settings.dashboard.SiteMapManager;
 import com.android.settings.testutils.SettingsRobolectricTestRunner;
@@ -33,6 +32,10 @@
 import org.robolectric.Robolectric;
 import org.robolectric.annotation.Config;
 
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
 @RunWith(SettingsRobolectricTestRunner.class)
 @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
 public class SearchFeatureProviderImplTest {
@@ -43,7 +46,7 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mActivity = Robolectric.buildActivity(Activity.class).create().visible().get();
-        mProvider = new SearchFeatureProviderImpl();
+        mProvider = spy(new SearchFeatureProviderImpl());
     }
 
     @Test
@@ -57,18 +60,19 @@
     @Test
     public void getDatabaseSearchLoader_shouldCleanupQuery() {
         final String query = "  space ";
-        final DatabaseResultLoader loader = mProvider.getDatabaseSearchLoader(mActivity, query);
 
-        assertThat(loader.mQueryText).isEqualTo(query.trim());
+        mProvider.getStaticSearchResultTask(mActivity, query);
+
+        verify(mProvider).cleanQuery(eq(query));
     }
 
     @Test
     public void getInstalledAppSearchLoader_shouldCleanupQuery() {
         final String query = "  space ";
-        final InstalledAppResultLoader loader =
-                mProvider.getInstalledAppSearchLoader(mActivity, query);
 
-        assertThat(loader.mQuery).isEqualTo(query.trim());
+        mProvider.getInstalledAppSearchTask(mActivity, query);
+
+        verify(mProvider).cleanQuery(eq(query));
     }
 
     @Test(expected = IllegalArgumentException.class)
@@ -87,4 +91,12 @@
         final ComponentName cn = new ComponentName(mActivity.getPackageName(), "class");
         mProvider.verifyLaunchSearchResultPageCaller(mActivity, cn);
     }
+
+    @Test
+    public void cleanQuery_trimsWhitespace() {
+        final String query = "  space ";
+        final String cleanQuery = "space";
+
+        assertThat(mProvider.cleanQuery(query)).isEqualTo(cleanQuery);
+    }
 }
diff --git a/tests/robotests/src/com/android/settings/search/SearchFragmentTest.java b/tests/robotests/src/com/android/settings/search/SearchFragmentTest.java
index b897008..a3f3334 100644
--- a/tests/robotests/src/com/android/settings/search/SearchFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/search/SearchFragmentTest.java
@@ -18,6 +18,7 @@
 package com.android.settings.search;
 
 import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyInt;
@@ -54,9 +55,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Answers;
-import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatcher;
-import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.robolectric.Robolectric;
@@ -65,7 +64,7 @@
 import org.robolectric.annotation.Config;
 import org.robolectric.util.ReflectionHelpers;
 
-import java.util.Set;
+import java.util.List;
 
 @RunWith(SettingsRobolectricTestRunner.class)
 @Config(manifest = TestConfig.MANIFEST_PATH,
@@ -79,22 +78,13 @@
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
     private Context mContext;
     @Mock
-    private DatabaseResultLoader mDatabaseResultLoader;
-    @Mock
-    private InstalledAppResultLoader mInstalledAppResultLoader;
-    @Mock
-    private AccessibilityServiceResultLoader mAccessibilityServiceResultLoader;
-    @Mock
-    private InputDeviceResultLoader mInputDeviceResultLoader;
-
+    private SearchResultLoader mSearchResultLoader;
     @Mock
     private SavedQueryLoader mSavedQueryLoader;
     @Mock
     private SavedQueryController mSavedQueryController;
     @Mock
     private SearchResultsAdapter mSearchResultsAdapter;
-    @Captor
-    private ArgumentCaptor<String> mQueryCaptor = ArgumentCaptor.forClass(String.class);
 
     private FakeFeatureFactory mFeatureFactory;
 
@@ -113,17 +103,8 @@
     @Test
     public void screenRotate_shouldPersistQuery() {
         when(mFeatureFactory.searchFeatureProvider
-                .getDatabaseSearchLoader(any(Context.class), anyString()))
-                .thenReturn(mDatabaseResultLoader);
-        when(mFeatureFactory.searchFeatureProvider
-                .getInstalledAppSearchLoader(any(Context.class), anyString()))
-                .thenReturn(mInstalledAppResultLoader);
-        when(mFeatureFactory.searchFeatureProvider
-                .getAccessibilityServiceResultLoader(any(Context.class), anyString()))
-                .thenReturn(mAccessibilityServiceResultLoader);
-        when(mFeatureFactory.searchFeatureProvider
-                .getInputDeviceResultLoader(any(Context.class), anyString()))
-                .thenReturn(mInputDeviceResultLoader);
+                .getSearchResultLoader(any(Context.class), anyString()))
+                .thenReturn(new MockSearchResultLoader(RuntimeEnvironment.application));
         when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
                 .thenReturn(mSavedQueryLoader);
 
@@ -168,25 +149,16 @@
         activityController.setup(bundle);
 
         verify(mFeatureFactory.searchFeatureProvider, never())
-                .getDatabaseSearchLoader(any(Context.class), anyString());
+                .getStaticSearchResultTask(any(Context.class), anyString());
         verify(mFeatureFactory.searchFeatureProvider, never())
-                .getInstalledAppSearchLoader(any(Context.class), anyString());
+                .getInstalledAppSearchTask(any(Context.class), anyString());
     }
 
     @Test
-    public void queryTextChange_shouldTriggerLoaderAndInitializeSearch() {
+    public void queryTextChange_shouldTriggerLoader() {
         when(mFeatureFactory.searchFeatureProvider
-                .getDatabaseSearchLoader(any(Context.class), anyString()))
-                .thenReturn(mDatabaseResultLoader);
-        when(mFeatureFactory.searchFeatureProvider
-                .getInstalledAppSearchLoader(any(Context.class), anyString()))
-                .thenReturn(mInstalledAppResultLoader);
-        when(mFeatureFactory.searchFeatureProvider
-                .getAccessibilityServiceResultLoader(any(Context.class), anyString()))
-                .thenReturn(mAccessibilityServiceResultLoader);
-        when(mFeatureFactory.searchFeatureProvider
-                .getInputDeviceResultLoader(any(Context.class), anyString()))
-                .thenReturn(mInputDeviceResultLoader);
+                .getSearchResultLoader(any(Context.class), anyString()))
+                .thenReturn(mSearchResultLoader);
         when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
                 .thenReturn(mSavedQueryLoader);
 
@@ -199,7 +171,6 @@
         when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
                 .thenReturn(true);
 
-        ReflectionHelpers.setField(fragment, "mSearchAdapter", mSearchResultsAdapter);
         fragment.onQueryTextChange(testQuery);
         activityController.get().onBackPressed();
 
@@ -209,11 +180,7 @@
                 any(Context.class),
                 eq(MetricsProto.MetricsEvent.ACTION_LEAVE_SEARCH_RESULT_WITHOUT_QUERY));
         verify(mFeatureFactory.searchFeatureProvider)
-                .getDatabaseSearchLoader(any(Context.class), anyString());
-        verify(mFeatureFactory.searchFeatureProvider)
-                .getInstalledAppSearchLoader(any(Context.class), anyString());
-        verify(mSearchResultsAdapter).initializeSearch(mQueryCaptor.capture());
-        assertThat(mQueryCaptor.getValue()).isEqualTo(testQuery);
+                .getSearchResultLoader(any(Context.class), anyString());
     }
 
     @Test
@@ -238,18 +205,6 @@
 
     @Test
     public void queryTextChangeToEmpty_shouldLoadSavedQueryAndNotInitializeSearch() {
-        when(mFeatureFactory.searchFeatureProvider
-                .getDatabaseSearchLoader(any(Context.class), anyString()))
-                .thenReturn(mDatabaseResultLoader);
-        when(mFeatureFactory.searchFeatureProvider
-                .getInstalledAppSearchLoader(any(Context.class), anyString()))
-                .thenReturn(mInstalledAppResultLoader);
-        when(mFeatureFactory.searchFeatureProvider
-                .getAccessibilityServiceResultLoader(any(Context.class), anyString()))
-                .thenReturn(mAccessibilityServiceResultLoader);
-        when(mFeatureFactory.searchFeatureProvider
-                .getInputDeviceResultLoader(any(Context.class), anyString()))
-                .thenReturn(mInputDeviceResultLoader);
         when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
                 .thenReturn(mSavedQueryLoader);
         ActivityController<SearchActivity> activityController =
@@ -266,27 +221,14 @@
         fragment.onQueryTextChange("");
 
         verify(mFeatureFactory.searchFeatureProvider, never())
-                .getDatabaseSearchLoader(any(Context.class), anyString());
+                .getStaticSearchResultTask(any(Context.class), anyString());
         verify(mFeatureFactory.searchFeatureProvider, never())
-                .getInstalledAppSearchLoader(any(Context.class), anyString());
+                .getInstalledAppSearchTask(any(Context.class), anyString());
         verify(mSavedQueryController).loadSavedQueries();
-        verify(mSearchResultsAdapter, never()).initializeSearch(anyString());
     }
 
     @Test
     public void updateIndex_TriggerOnCreate() {
-        when(mFeatureFactory.searchFeatureProvider
-                .getDatabaseSearchLoader(any(Context.class), anyString()))
-                .thenReturn(mDatabaseResultLoader);
-        when(mFeatureFactory.searchFeatureProvider
-                .getInstalledAppSearchLoader(any(Context.class), anyString()))
-                .thenReturn(mInstalledAppResultLoader);
-        when(mFeatureFactory.searchFeatureProvider
-                .getAccessibilityServiceResultLoader(any(Context.class), anyString()))
-                .thenReturn(mAccessibilityServiceResultLoader);
-        when(mFeatureFactory.searchFeatureProvider
-                .getInputDeviceResultLoader(any(Context.class), anyString()))
-                .thenReturn(mInputDeviceResultLoader);
         when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
                 .thenReturn(mSavedQueryLoader);
 
@@ -304,40 +246,10 @@
     }
 
     @Test
-    public void syncLoaders_MergeWhenAllLoadersDone() {
-        when(mFeatureFactory.searchFeatureProvider
-                .getDatabaseSearchLoader(any(Context.class), anyString()))
-                .thenReturn(new MockDBLoader(RuntimeEnvironment.application));
-        when(mFeatureFactory.searchFeatureProvider
-                .getInstalledAppSearchLoader(any(Context.class), anyString()))
-                .thenReturn(new MockAppLoader(RuntimeEnvironment.application));
-        when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
-                .thenReturn(mSavedQueryLoader);
-
-        ActivityController<SearchActivity> activityController =
-                Robolectric.buildActivity(SearchActivity.class);
-        activityController.setup();
-
-        SearchFragment fragment = (SearchFragment) spy(activityController.get().getFragmentManager()
-                .findFragmentById(R.id.main_content));
-        when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
-                .thenReturn(true);
-
-        fragment.onQueryTextChange("non-empty");
-
-        Robolectric.flushForegroundThreadScheduler();
-
-        verify(fragment, times(2)).onLoadFinished(any(Loader.class), any(Set.class));
-    }
-
-    @Test
     public void whenNoQuery_HideFeedbackIsCalled() {
         when(mFeatureFactory.searchFeatureProvider
-                .getDatabaseSearchLoader(any(Context.class), anyString()))
-                .thenReturn(new MockDBLoader(RuntimeEnvironment.application));
-        when(mFeatureFactory.searchFeatureProvider
-                .getInstalledAppSearchLoader(any(Context.class), anyString()))
-                .thenReturn(new MockAppLoader(RuntimeEnvironment.application));
+                .getSearchResultLoader(any(Context.class), anyString()))
+                .thenReturn(new MockSearchResultLoader(RuntimeEnvironment.application));
         when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
                 .thenReturn(mSavedQueryLoader);
 
@@ -359,17 +271,8 @@
     @Test
     public void onLoadFinished_ShowsFeedback() {
         when(mFeatureFactory.searchFeatureProvider
-                .getDatabaseSearchLoader(any(Context.class), anyString()))
-                .thenReturn(new MockDBLoader(RuntimeEnvironment.application));
-        when(mFeatureFactory.searchFeatureProvider
-                .getInstalledAppSearchLoader(any(Context.class), anyString()))
-                .thenReturn(new MockAppLoader(RuntimeEnvironment.application));
-        when(mFeatureFactory.searchFeatureProvider
-                .getAccessibilityServiceResultLoader(any(Context.class), anyString()))
-                .thenReturn(new MockAccessibilityLoader(RuntimeEnvironment.application));
-        when(mFeatureFactory.searchFeatureProvider
-                .getInputDeviceResultLoader(any(Context.class), anyString()))
-                .thenReturn(new MockInputDeviceResultLoader(RuntimeEnvironment.application));
+                .getSearchResultLoader(any(Context.class), anyString()))
+                .thenReturn(new MockSearchResultLoader(RuntimeEnvironment.application));
         when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
                 .thenReturn(mSavedQueryLoader);
         ActivityController<SearchActivity> activityController =
@@ -413,9 +316,7 @@
 
         fragment.onIndexingFinished();
 
-        verify(loaderManager).initLoader(eq(SearchFragment.SearchLoaderId.DATABASE),
-                eq(null), any(LoaderManager.LoaderCallbacks.class));
-        verify(loaderManager).initLoader(eq(SearchFragment.SearchLoaderId.INSTALLED_APPS),
+        verify(loaderManager).initLoader(eq(SearchFragment.SearchLoaderId.SEARCH_RESULT),
                 eq(null), any(LoaderManager.LoaderCallbacks.class));
     }
 
@@ -480,16 +381,13 @@
                 eq("test_setting"),
                 argThat(pairMatches(MetricsProto.MetricsEvent.FIELD_SETTINGS_SEARCH_RESULT_COUNT)),
                 argThat(pairMatches(MetricsProto.MetricsEvent.FIELD_SETTINGS_SEARCH_RESULT_RANK)),
-                argThat(pairMatches(MetricsProto.MetricsEvent
-                                .FIELD_SETTINGS_SEARCH_RESULT_ASYNC_RANKING_STATE)),
                 argThat(pairMatches(MetricsProto.MetricsEvent.FIELD_SETTINGS_SEARCH_QUERY_LENGTH)));
-
         verify(mFeatureFactory.searchFeatureProvider).searchResultClicked(nullable(Context.class),
                 nullable(String.class), eq(searchResult));
     }
 
     @Test
-    public void onResume_shouldCallSearchRankingWarmupIfSmartSearchRankingEnabled(){
+    public void onResume_shouldCallSearchRankingWarmupIfSmartSearchRankingEnabled() {
         when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(any(Context.class)))
                 .thenReturn(true);
 
@@ -504,7 +402,7 @@
     }
 
     @Test
-    public void onResume_shouldNotCallSearchRankingWarmupIfSmartSearchRankingDisabled(){
+    public void onResume_shouldNotCallSearchRankingWarmupIfSmartSearchRankingDisabled() {
         when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(any(Context.class)))
                 .thenReturn(false);
 
diff --git a/tests/robotests/src/com/android/settings/search/SearchResultAggregatorTest.java b/tests/robotests/src/com/android/settings/search/SearchResultAggregatorTest.java
new file mode 100644
index 0000000..286d7cf9
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/search/SearchResultAggregatorTest.java
@@ -0,0 +1,271 @@
+package com.android.settings.search;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.settings.TestConfig;
+import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class SearchResultAggregatorTest {
+
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private Context mContext;
+
+    private FakeFeatureFactory mFeatureFactory;
+
+    private SearchResultAggregator mAggregator;
+
+    @Mock
+    private DatabaseResultLoader mStaticTask;
+    @Mock
+    private InstalledAppResultLoader mAppTask;
+    @Mock
+    private InputDeviceResultLoader mInputTask;
+    @Mock
+    private AccessibilityServiceResultLoader mMAccessibilityTask;
+    @Mock
+    private ExecutorService mService;
+
+
+    private String[] DB_TITLES = {"static_one", "static_two"};
+    private String[] INPUT_TITLES = {"input_one", "input_two"};
+    private String[] ACCESS_TITLES = {"access_one", "access_two"};
+    private String[] APP_TITLES = {"app_one", "app_two"};
+
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mAggregator = spy(SearchResultAggregator.getInstance());
+        FakeFeatureFactory.setupForTest(mContext);
+        mFeatureFactory = (FakeFeatureFactory) FakeFeatureFactory.getFactory(mContext);
+
+        // Return mock loaders from feature provider
+        when(mFeatureFactory.searchFeatureProvider.getStaticSearchResultTask(any(Context.class),
+                anyString())).thenReturn(mStaticTask);
+        when(mFeatureFactory.searchFeatureProvider.getInstalledAppSearchTask(any(Context.class),
+                anyString())).thenReturn(mAppTask);
+        when(mFeatureFactory.searchFeatureProvider.getInputDeviceResultTask(any(Context.class),
+                anyString())).thenReturn(mInputTask);
+        when(mFeatureFactory.searchFeatureProvider.getAccessibilityServiceResultTask(
+                any(Context.class),
+                anyString())).thenReturn(mMAccessibilityTask);
+        when(mFeatureFactory.searchFeatureProvider.getExecutorService()).thenReturn(mService);
+
+        // Return fake data from the loaders
+        List<? extends SearchResult> dbResults = getDummyDbResults();
+        doReturn(dbResults).when(mStaticTask).get(anyLong(), any(TimeUnit.class));
+
+        List<? extends SearchResult> appResults = getDummyAppResults();
+        doReturn(appResults).when(mAppTask).get(anyLong(), any(TimeUnit.class));
+
+        List<? extends SearchResult> inputResults = getDummyInputDeviceResults();
+        doReturn(inputResults).when(mInputTask).get(anyLong(), any(TimeUnit.class));
+
+        List<? extends SearchResult> accessResults = getDummyAccessibilityResults();
+        doReturn(accessResults).when(mMAccessibilityTask).get(anyLong(), any(TimeUnit.class));
+    }
+
+    @Test
+    public void testStaticResults_mergedProperly() {
+        when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(mContext))
+                .thenReturn(false);
+
+        List<? extends SearchResult> results = mAggregator.fetchResults(mContext, "test");
+
+        assertThat(results).hasSize(8);
+        assertThat(results.get(0).title).isEqualTo(DB_TITLES[0]);
+        assertThat(results.get(1).title).isEqualTo(DB_TITLES[1]);
+        assertThat(results.get(2).title).isEqualTo(APP_TITLES[0]);
+        assertThat(results.get(3).title).isEqualTo(ACCESS_TITLES[0]);
+        assertThat(results.get(4).title).isEqualTo(INPUT_TITLES[0]);
+        assertThat(results.get(5).title).isEqualTo(APP_TITLES[1]);
+        assertThat(results.get(6).title).isEqualTo(ACCESS_TITLES[1]);
+        assertThat(results.get(7).title).isEqualTo(INPUT_TITLES[1]);
+    }
+
+    @Test
+    public void testStaticRanking_staticThrowsException_dbResultsAreMissing() throws Exception {
+        when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(mContext))
+                .thenReturn(false);
+        when(mStaticTask.get(anyLong(), any(TimeUnit.class))).thenThrow(new InterruptedException());
+
+        List<? extends SearchResult> results = mAggregator.fetchResults(mContext, "test");
+
+        assertThat(results).hasSize(6);
+        assertThat(results.get(0).title).isEqualTo(APP_TITLES[0]);
+        assertThat(results.get(1).title).isEqualTo(ACCESS_TITLES[0]);
+        assertThat(results.get(2).title).isEqualTo(INPUT_TITLES[0]);
+        assertThat(results.get(3).title).isEqualTo(APP_TITLES[1]);
+        assertThat(results.get(4).title).isEqualTo(ACCESS_TITLES[1]);
+        assertThat(results.get(5).title).isEqualTo(INPUT_TITLES[1]);
+    }
+
+    @Test
+    public void testStaticRanking_appsThrowException_appResultsAreMissing() throws Exception {
+        when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(mContext))
+                .thenReturn(false);
+        when(mAppTask.get(anyLong(), any(TimeUnit.class))).thenThrow(new InterruptedException());
+
+        List<? extends SearchResult> results = mAggregator.fetchResults(mContext, "test");
+
+        assertThat(results).hasSize(6);
+        assertThat(results.get(0).title).isEqualTo(DB_TITLES[0]);
+        assertThat(results.get(1).title).isEqualTo(DB_TITLES[1]);
+        assertThat(results.get(2).title).isEqualTo(ACCESS_TITLES[0]);
+        assertThat(results.get(3).title).isEqualTo(INPUT_TITLES[0]);
+        assertThat(results.get(4).title).isEqualTo(ACCESS_TITLES[1]);
+        assertThat(results.get(5).title).isEqualTo(INPUT_TITLES[1]);
+    }
+
+    @Test
+    public void testStaticRanking_inputThrowException_inputResultsAreMissing() throws Exception {
+        when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(mContext))
+                .thenReturn(false);
+        when(mInputTask.get(anyLong(), any(TimeUnit.class))).thenThrow(new InterruptedException());
+
+        List<? extends SearchResult> results = mAggregator.fetchResults(mContext, "test");
+
+        assertThat(results).hasSize(6);
+        assertThat(results.get(0).title).isEqualTo(DB_TITLES[0]);
+        assertThat(results.get(1).title).isEqualTo(DB_TITLES[1]);
+        assertThat(results.get(2).title).isEqualTo(APP_TITLES[0]);
+        assertThat(results.get(3).title).isEqualTo(ACCESS_TITLES[0]);
+        assertThat(results.get(4).title).isEqualTo(APP_TITLES[1]);
+        assertThat(results.get(5).title).isEqualTo(ACCESS_TITLES[1]);
+    }
+
+    @Test
+    public void testStaticRanking_accessThrowException_accessResultsAreMissing() throws Exception {
+        when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(mContext))
+                .thenReturn(false);
+        when(mMAccessibilityTask.get(anyLong(), any(TimeUnit.class))).thenThrow(
+                new InterruptedException());
+
+        List<? extends SearchResult> results = mAggregator.fetchResults(mContext, "test");
+
+        assertThat(results).hasSize(6);
+        assertThat(results.get(0).title).isEqualTo(DB_TITLES[0]);
+        assertThat(results.get(1).title).isEqualTo(DB_TITLES[1]);
+        assertThat(results.get(2).title).isEqualTo(APP_TITLES[0]);
+        assertThat(results.get(3).title).isEqualTo(INPUT_TITLES[0]);
+        assertThat(results.get(4).title).isEqualTo(APP_TITLES[1]);
+        assertThat(results.get(5).title).isEqualTo(INPUT_TITLES[1]);
+    }
+
+    @Test
+    public void testDynamicRanking_sortsWithDynamicRanking() {
+        when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(
+                true);
+
+        List<? extends SearchResult> results = mAggregator.fetchResults(mContext, "test");
+
+        assertThat(results).hasSize(8);
+        assertThat(results.get(0).title).isEqualTo(DB_TITLES[0]);
+        assertThat(results.get(1).title).isEqualTo(DB_TITLES[1]);
+        assertThat(results.get(2).title).isEqualTo(APP_TITLES[0]);
+        assertThat(results.get(3).title).isEqualTo(ACCESS_TITLES[0]);
+        assertThat(results.get(4).title).isEqualTo(INPUT_TITLES[0]);
+        assertThat(results.get(5).title).isEqualTo(APP_TITLES[1]);
+        assertThat(results.get(6).title).isEqualTo(ACCESS_TITLES[1]);
+        assertThat(results.get(7).title).isEqualTo(INPUT_TITLES[1]);
+    }
+
+    private List<? extends SearchResult> getDummyDbResults() {
+        List<SearchResult> results = new ArrayList<>();
+        ResultPayload payload = new ResultPayload(new Intent());
+        SearchResult.Builder builder = new SearchResult.Builder();
+        builder.setPayload(payload)
+                .setTitle(DB_TITLES[0])
+                .setRank(1)
+                .setStableId(Objects.hash(DB_TITLES[0], "db"));
+        results.add(builder.build());
+
+        builder.setTitle(DB_TITLES[1])
+                .setRank(2)
+                .setStableId(Objects.hash(DB_TITLES[1], "db"));
+        results.add(builder.build());
+
+        return results;
+    }
+
+    private List<? extends SearchResult> getDummyAppResults() {
+        List<AppSearchResult> results = new ArrayList<>();
+        ResultPayload payload = new ResultPayload(new Intent());
+        AppSearchResult.Builder builder = new AppSearchResult.Builder();
+        builder.setPayload(payload)
+                .setTitle(APP_TITLES[0])
+                .setRank(1)
+                .setStableId(Objects.hash(APP_TITLES[0], "app"));
+        results.add(builder.build());
+
+        builder.setTitle(APP_TITLES[1])
+                .setRank(2)
+                .setStableId(Objects.hash(APP_TITLES[1], "app"));
+        results.add(builder.build());
+
+        return results;
+    }
+
+    public List<? extends SearchResult> getDummyInputDeviceResults() {
+        List<SearchResult> results = new ArrayList<>();
+        ResultPayload payload = new ResultPayload(new Intent());
+        AppSearchResult.Builder builder = new AppSearchResult.Builder();
+        builder.setPayload(payload)
+                .setTitle(INPUT_TITLES[0])
+                .setRank(1)
+                .setStableId(Objects.hash(INPUT_TITLES[0], "app"));
+        results.add(builder.build());
+
+        builder.setTitle(INPUT_TITLES[1])
+                .setRank(2)
+                .setStableId(Objects.hash(INPUT_TITLES[1], "app"));
+        results.add(builder.build());
+
+        return results;
+    }
+
+    public List<? extends SearchResult> getDummyAccessibilityResults() {
+        List<SearchResult> results = new ArrayList<>();
+        ResultPayload payload = new ResultPayload(new Intent());
+        AppSearchResult.Builder builder = new AppSearchResult.Builder();
+        builder.setPayload(payload)
+                .setTitle(ACCESS_TITLES[0])
+                .setRank(1)
+                .setStableId(Objects.hash(ACCESS_TITLES[0], "app"));
+        results.add(builder.build());
+
+        builder.setTitle(ACCESS_TITLES[1])
+                .setRank(2)
+                .setStableId(Objects.hash(ACCESS_TITLES[1], "app"));
+        results.add(builder.build());
+
+        return results;
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/search/SearchResultsAdapterTest.java b/tests/robotests/src/com/android/settings/search/SearchResultsAdapterTest.java
index a2afb35..4baf8d2 100644
--- a/tests/robotests/src/com/android/settings/search/SearchResultsAdapterTest.java
+++ b/tests/robotests/src/com/android/settings/search/SearchResultsAdapterTest.java
@@ -18,44 +18,32 @@
 package com.android.settings.search;
 
 import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
-import android.graphics.drawable.Drawable;
-import android.util.Pair;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
 
-import com.android.settings.R;
 import com.android.settings.TestConfig;
-import com.android.settings.search.SearchResult.Builder;
-import com.android.settings.search.ranking.SearchResultsRankerCallback;
 import com.android.settings.testutils.SettingsRobolectricTestRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.robolectric.Robolectric;
 import org.robolectric.annotation.Config;
-import org.robolectric.shadows.ShadowLooper;
 
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
 
 @RunWith(SettingsRobolectricTestRunner.class)
 @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
@@ -67,25 +55,18 @@
     private SearchFeatureProvider mSearchFeatureProvider;
     @Mock
     private Context mMockContext;
-    @Captor
-    private ArgumentCaptor<Integer> mSearchResultsCountCaptor =
-            ArgumentCaptor.forClass(Integer.class);
     private SearchResultsAdapter mAdapter;
     private Context mContext;
-    private String mLoaderClassName;
-
-    private String[] TITLES = {"alpha", "bravo", "charlie", "appAlpha", "appBravo", "appCharlie"};
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mContext = Robolectric.buildActivity(Activity.class).get();
-        mLoaderClassName = DatabaseResultLoader.class.getName();
         when(mFragment.getContext()).thenReturn(mMockContext);
         when(mMockContext.getApplicationContext()).thenReturn(mContext);
         when(mSearchFeatureProvider.smartSearchRankingTimeoutMs(any(Context.class)))
                 .thenReturn(300L);
-        mAdapter = new SearchResultsAdapter(mFragment, mSearchFeatureProvider);
+        mAdapter = new SearchResultsAdapter(mFragment);
     }
 
     @Test
@@ -95,17 +76,6 @@
     }
 
     @Test
-    public void testSingleSourceMerge_exactCopyReturned() {
-        Set<SearchResult> intentResults = getIntentSampleResults();
-        mAdapter.initializeSearch("");
-        mAdapter.addSearchResults(intentResults, mLoaderClassName);
-        mAdapter.notifyResultsLoaded();
-
-        List<SearchResult> updatedResults = mAdapter.getSearchResults();
-        assertThat(updatedResults).containsAllIn(intentResults);
-    }
-
-    @Test
     public void testCreateViewHolder_returnsIntentResult() {
         ViewGroup group = new FrameLayout(mContext);
         SearchViewHolder view = mAdapter.onCreateViewHolder(group,
@@ -123,387 +93,13 @@
     }
 
     @Test
-    public void testEndToEndSearch_properResultsMerged_correctOrder() {
-        mAdapter.initializeSearch("");
-        mAdapter.addSearchResults(new HashSet<>(getDummyAppResults()),
-                InstalledAppResultLoader.class.getName());
-        mAdapter.addSearchResults(new HashSet<>(getDummyDbResults()),
-                DatabaseResultLoader.class.getName());
-        mAdapter.notifyResultsLoaded();
+    public void testPostSearchResults_addsDataAndDisplays() {
+        List<SearchResult> results = getDummyDbResults();
 
-        List<SearchResult> results = mAdapter.getSearchResults();
-        assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha
-        assertThat(results.get(1).title).isEqualTo(TITLES[3]); // appAlpha
-        assertThat(results.get(2).title).isEqualTo(TITLES[4]); // appBravo
-        assertThat(results.get(3).title).isEqualTo(TITLES[1]); // bravo
-        assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
-        assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
-        verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
-        assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
-    }
+        mAdapter.postSearchResults(results);
 
-    @Test
-    public void testEndToEndSearch_addResults_resultsAddedInOrder() {
-        List<SearchResult> appResults = getDummyAppResults();
-        List<SearchResult> dbResults = getDummyDbResults();
-        mAdapter.initializeSearch("");
-        // Add two individual items
-        mAdapter.addSearchResults(new HashSet<>(appResults.subList(0, 1)),
-                InstalledAppResultLoader.class.getName());
-        mAdapter.addSearchResults(new HashSet<>(dbResults.subList(0, 1)),
-                DatabaseResultLoader.class.getName());
-        mAdapter.notifyResultsLoaded();
-        // Add super-set of items
-        mAdapter.initializeSearch("");
-        mAdapter.addSearchResults(
-                new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
-        mAdapter.addSearchResults(
-                new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
-        mAdapter.notifyResultsLoaded();
-
-        List<SearchResult> results = mAdapter.getSearchResults();
-        assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha
-        assertThat(results.get(1).title).isEqualTo(TITLES[3]); // appAlpha
-        assertThat(results.get(2).title).isEqualTo(TITLES[4]); // appBravo
-        assertThat(results.get(3).title).isEqualTo(TITLES[1]); // bravo
-        assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
-        assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
-        verify(mFragment, times(2)).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
-        assertThat(mSearchResultsCountCaptor.getAllValues().toArray())
-                .isEqualTo(new Integer[] {2, 6});
-    }
-
-    @Test
-    public void testEndToEndSearch_removeResults_resultsAdded() {
-        List<SearchResult> appResults = getDummyAppResults();
-        List<SearchResult> dbResults = getDummyDbResults();
-        // Add list of items
-        mAdapter.initializeSearch("");
-        mAdapter.addSearchResults(new HashSet<>(appResults),
-                InstalledAppResultLoader.class.getName());
-        mAdapter.addSearchResults(new HashSet<>(dbResults),
-                DatabaseResultLoader.class.getName());
-        mAdapter.notifyResultsLoaded();
-        // Add subset of items
-        mAdapter.initializeSearch("");
-        mAdapter.addSearchResults(new HashSet<>(appResults.subList(0, 1)),
-                InstalledAppResultLoader.class.getName());
-        mAdapter.addSearchResults(new HashSet<>(dbResults.subList(0, 1)),
-                DatabaseResultLoader.class.getName());
-        mAdapter.notifyResultsLoaded();
-
-        List<SearchResult> results = mAdapter.getSearchResults();
-        assertThat(results.get(0).title).isEqualTo(TITLES[0]);
-        assertThat(results.get(1).title).isEqualTo(TITLES[3]);
-        verify(mFragment, times(2)).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
-        assertThat(mSearchResultsCountCaptor.getAllValues().toArray())
-                .isEqualTo(new Integer[] {6, 2});
-    }
-    @Test
-    public void testEndToEndSearch_smartSearchRankingEnabledAndSucceededAfterResultsLoaded() {
-        when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
-
-        List<SearchResult> appResults = getDummyAppResults();
-        List<SearchResult> dbResults = getDummyDbResults();
-        mAdapter.initializeSearch("");
-        mAdapter.addSearchResults(
-                new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
-        mAdapter.addSearchResults(
-                new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
-        mAdapter.notifyResultsLoaded();
-        mAdapter.onRankingScoresAvailable(getDummyRankingScores());
-
-        List<SearchResult> results = mAdapter.getSearchResults();
-        assertThat(results.get(0).title).isEqualTo(TITLES[2]); // charlie
-        assertThat(results.get(1).title).isEqualTo(TITLES[0]); // alpha
-        assertThat(results.get(2).title).isEqualTo(TITLES[1]); // bravo
-        assertThat(results.get(3).title).isEqualTo(TITLES[3]); // appAlpha
-        assertThat(results.get(4).title).isEqualTo(TITLES[4]); // appBravo
-        assertThat(results.get(5).title).isEqualTo(TITLES[5]); // appCharlie
-        verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
-        assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
-        assertThat(mAdapter.getAsyncRankingState()).isEqualTo(SearchResultsAdapter.SUCCEEDED);
-    }
-
-    @Test
-    public void testEndToEndSearch_smartSearchRankingEnabledAndSucceededBeforeResultsLoaded() {
-        when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
-
-        List<SearchResult> appResults = getDummyAppResults();
-        List<SearchResult> dbResults = getDummyDbResults();
-        mAdapter.initializeSearch("");
-        mAdapter.onRankingScoresAvailable(getDummyRankingScores());
-        mAdapter.addSearchResults(
-                new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
-        mAdapter.addSearchResults(
-                new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
-        mAdapter.notifyResultsLoaded();
-
-        List<SearchResult> results = mAdapter.getSearchResults();
-        assertThat(results.get(0).title).isEqualTo(TITLES[2]); // charlie
-        assertThat(results.get(1).title).isEqualTo(TITLES[0]); // alpha
-        assertThat(results.get(2).title).isEqualTo(TITLES[1]); // bravo
-        assertThat(results.get(3).title).isEqualTo(TITLES[3]); // appAlpha
-        assertThat(results.get(4).title).isEqualTo(TITLES[4]); // appBravo
-        assertThat(results.get(5).title).isEqualTo(TITLES[5]); // appCharlie
-        verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
-        assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
-        assertThat(mAdapter.getAsyncRankingState()).isEqualTo(SearchResultsAdapter.SUCCEEDED);
-    }
-
-    @Test
-    public void testEndToEndSearch_smartSearchRankingEnabledAndFailedAfterResultsLoaded() {
-        when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
-
-        List<SearchResult> appResults = getDummyAppResults();
-        List<SearchResult> dbResults = getDummyDbResults();
-        mAdapter.initializeSearch("");
-        mAdapter.addSearchResults(
-                new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
-        mAdapter.addSearchResults(
-                new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
-        mAdapter.notifyResultsLoaded();
-        mAdapter.onRankingFailed();
-
-        List<SearchResult> results = mAdapter.getSearchResults();
-        assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha
-        assertThat(results.get(1).title).isEqualTo(TITLES[3]); // appAlpha
-        assertThat(results.get(2).title).isEqualTo(TITLES[4]); // appBravo
-        assertThat(results.get(3).title).isEqualTo(TITLES[1]); // bravo
-        assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
-        assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
-        verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
-        assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
-        assertThat(mAdapter.getAsyncRankingState()).isEqualTo(SearchResultsAdapter.FAILED);
-    }
-
-    @Test
-    public void testEndToEndSearch_smartSearchRankingEnabledAndFailedBeforeResultsLoaded() {
-        when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
-
-        List<SearchResult> appResults = getDummyAppResults();
-        List<SearchResult> dbResults = getDummyDbResults();
-        mAdapter.initializeSearch("");
-        mAdapter.onRankingFailed();
-        mAdapter.addSearchResults(
-                new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
-        mAdapter.addSearchResults(
-                new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
-        mAdapter.notifyResultsLoaded();
-
-        List<SearchResult> results = mAdapter.getSearchResults();
-        assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha
-        assertThat(results.get(1).title).isEqualTo(TITLES[3]); // appAlpha
-        assertThat(results.get(2).title).isEqualTo(TITLES[4]); // appBravo
-        assertThat(results.get(3).title).isEqualTo(TITLES[1]); // bravo
-        assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
-        assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
-        verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
-        assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
-        assertThat(mAdapter.getAsyncRankingState()).isEqualTo(SearchResultsAdapter.FAILED);
-    }
-
-    @Test
-    public void testEndToEndSearch_smartSearchRankingEnabledAndTimedoutAfterResultsLoaded() {
-        when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
-
-        List<SearchResult> appResults = getDummyAppResults();
-        List<SearchResult> dbResults = getDummyDbResults();
-        mAdapter.initializeSearch("");
-        mAdapter.addSearchResults(
-                new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
-        mAdapter.addSearchResults(
-                new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
-        mAdapter.notifyResultsLoaded();
-
-        waitUntilRankingTimesOut();
-
-        List<SearchResult> results = mAdapter.getSearchResults();
-        assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha
-        assertThat(results.get(1).title).isEqualTo(TITLES[3]); // appAlpha
-        assertThat(results.get(2).title).isEqualTo(TITLES[4]); // appBravo
-        assertThat(results.get(3).title).isEqualTo(TITLES[1]); // bravo
-        assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
-        assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
-        verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
-        assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
-        assertThat(mAdapter.getAsyncRankingState()).isEqualTo(SearchResultsAdapter.TIMED_OUT);
-    }
-
-    @Test
-    public void testEndToEndSearch_smartSearchRankingEnabledAndTimedoutBeforeResultsLoaded() {
-        when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
-
-        List<SearchResult> appResults = getDummyAppResults();
-        List<SearchResult> dbResults = getDummyDbResults();
-        mAdapter.initializeSearch("");
-
-        waitUntilRankingTimesOut();
-
-        mAdapter.addSearchResults(
-                new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
-        mAdapter.addSearchResults(
-                new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
-        mAdapter.notifyResultsLoaded();
-
-        List<SearchResult> results = mAdapter.getSearchResults();
-        assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha
-        assertThat(results.get(1).title).isEqualTo(TITLES[3]); // appAlpha
-        assertThat(results.get(2).title).isEqualTo(TITLES[4]); // appBravo
-        assertThat(results.get(3).title).isEqualTo(TITLES[1]); // bravo
-        assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
-        assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
-        verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
-        assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
-        assertThat(mAdapter.getAsyncRankingState()).isEqualTo(SearchResultsAdapter.TIMED_OUT);
-    }
-
-    @Test
-    public void testDoSmartRanking_shouldRankAppResultsAfterDbResults() {
-        when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
-
-        List<SearchResult> appResults = getDummyAppResults();
-        List<SearchResult> dbResults = getDummyDbResults();
-        mAdapter.initializeSearch("");
-        mAdapter.addSearchResults(
-                new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
-        mAdapter.addSearchResults(
-                new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
-        mAdapter.notifyResultsLoaded();
-        mAdapter.onRankingScoresAvailable(getDummyRankingScores());
-        List<SearchResult> results = mAdapter.doAsyncRanking();
-        assertThat(results.get(0).title).isEqualTo(TITLES[2]); // charlie
-        assertThat(results.get(1).title).isEqualTo(TITLES[0]); // alpha
-        assertThat(results.get(2).title).isEqualTo(TITLES[1]); // bravo
-        assertThat(results.get(3).title).isEqualTo(TITLES[3]); // appAlpha
-        assertThat(results.get(4).title).isEqualTo(TITLES[4]); // appBravo
-        assertThat(results.get(5).title).isEqualTo(TITLES[5]); // appCharlie
-    }
-
-    @Test
-    public void testDoSmartRanking_shouldRankResultsWithMissingScoresAfterScoredResults() {
-        when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
-
-        List<SearchResult> appResults = getDummyAppResults();
-        List<SearchResult> dbResults = getDummyDbResults();
-        mAdapter.initializeSearch("");
-        mAdapter.addSearchResults(
-                new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
-        mAdapter.addSearchResults(
-                new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
-        mAdapter.notifyResultsLoaded();
-        List<Pair<String, Float>> rankingScores = getDummyRankingScores();
-        rankingScores.remove(1); // no ranking score for alpha
-        mAdapter.onRankingScoresAvailable(rankingScores);
-        List<SearchResult> results = mAdapter.doAsyncRanking();
-        assertThat(results.get(0).title).isEqualTo(TITLES[2]); // charlie
-        assertThat(results.get(1).title).isEqualTo(TITLES[1]); // bravo
-        assertThat(results.get(2).title).isEqualTo(TITLES[0]); // alpha
-        assertThat(results.get(3).title).isEqualTo(TITLES[3]); // appAlpha
-        assertThat(results.get(4).title).isEqualTo(TITLES[4]); // appBravo
-        assertThat(results.get(5).title).isEqualTo(TITLES[5]); // appCharlie
-    }
-
-    @Test
-    public void testGetUnsortedLoadedResults () {
-        List<SearchResult> appResults = getDummyAppResults();
-        List<SearchResult> dbResults = getDummyDbResults();
-        mAdapter.initializeSearch("");
-        mAdapter.addSearchResults(
-                new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
-        mAdapter.addSearchResults(
-                new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
-        Set<CharSequence> expectedDbTitles = new HashSet<>(
-                Arrays.asList("alpha", "bravo", "charlie"));
-        Set<CharSequence> expectedAppTitles = new HashSet<>(
-                Arrays.asList("appAlpha", "appBravo", "appCharlie"));
-        Set<CharSequence> actualDbTitles = new HashSet<>();
-        Set<CharSequence> actualAppTitles = new HashSet<>();
-        for (SearchResult result : mAdapter.getUnsortedLoadedResults(SearchResultsAdapter
-                .DB_RESULTS_LOADER_KEY)) {
-            actualDbTitles.add(result.title);
-        }
-        for (SearchResult result : mAdapter.getUnsortedLoadedResults(SearchResultsAdapter
-                .APP_RESULTS_LOADER_KEY)) {
-            actualAppTitles.add(result.title);
-        }
-        assertThat(actualDbTitles).isEqualTo(expectedDbTitles);
-        assertThat(actualAppTitles).isEqualTo(expectedAppTitles);
-    }
-
-    @Test
-    public void testGetSortedLoadedResults() {
-        List<SearchResult> appResults = getDummyAppResults();
-        List<SearchResult> dbResults = getDummyDbResults();
-        mAdapter.initializeSearch("");
-        mAdapter.addSearchResults(
-                new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
-        mAdapter.addSearchResults(
-                new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
-        List<? extends SearchResult> actualDbResults =
-                mAdapter.getSortedLoadedResults(SearchResultsAdapter.DB_RESULTS_LOADER_KEY);
-        List<? extends SearchResult> actualAppResults =
-                mAdapter.getSortedLoadedResults(SearchResultsAdapter.APP_RESULTS_LOADER_KEY);
-        assertThat(actualDbResults.get(0).title).isEqualTo(TITLES[0]); // charlie
-        assertThat(actualDbResults.get(1).title).isEqualTo(TITLES[1]); // bravo
-        assertThat(actualDbResults.get(2).title).isEqualTo(TITLES[2]); // alpha
-        assertThat(actualAppResults.get(0).title).isEqualTo(TITLES[3]); // appAlpha
-        assertThat(actualAppResults.get(1).title).isEqualTo(TITLES[4]); // appBravo
-        assertThat(actualAppResults.get(2).title).isEqualTo(TITLES[5]); // appCharlie
-    }
-
-    @Test
-    public void testInitializeSearch_shouldNotRunSmartRankingIfDisabled() {
-        when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(false);
-        mAdapter.initializeSearch("");
-        mAdapter.notifyResultsLoaded();
-        verify(mSearchFeatureProvider, never()).querySearchResults(
-                any(Context.class), anyString(), any(SearchResultsRankerCallback.class));
-        assertThat(mAdapter.getAsyncRankingState()).isEqualTo(SearchResultsAdapter.DISABLED);
-    }
-
-    @Test
-    public void testInitialSearch_shouldRunSmartRankingIfEnabled() {
-        when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
-        mAdapter.initializeSearch("");
-        mAdapter.notifyResultsLoaded();
-        verify(mSearchFeatureProvider, times(1)).querySearchResults(
-                any(Context.class), anyString(), any(SearchResultsRankerCallback.class));
-        assertThat(mAdapter.getAsyncRankingState())
-                .isEqualTo(SearchResultsAdapter.PENDING_RESULTS);
-    }
-
-    @Test
-    public void testGetRankingScoreByStableId() {
-        when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
-
-        List<SearchResult> appResults = getDummyAppResults();
-        List<SearchResult> dbResults = getDummyDbResults();
-        mAdapter.initializeSearch("");
-        mAdapter.onRankingScoresAvailable(getDummyRankingScores());
-        assertThat(mAdapter.getRankingScoreByStableId(dbResults.get(0).stableId))
-                .isWithin(1e-10f).of(0.8f);
-        assertThat(mAdapter.getRankingScoreByStableId(dbResults.get(1).stableId))
-                .isWithin(1e-10f).of(0.2f);
-        assertThat(mAdapter.getRankingScoreByStableId(dbResults.get(2).stableId))
-                .isWithin(1e-10f).of(0.9f);
-        assertThat(mAdapter.getRankingScoreByStableId(appResults.get(0).stableId))
-                .isEqualTo(-Float.MAX_VALUE);
-        assertThat(mAdapter.getRankingScoreByStableId(appResults.get(1).stableId))
-                .isEqualTo(-Float.MAX_VALUE);
-        assertThat(mAdapter.getRankingScoreByStableId(appResults.get(2).stableId))
-                .isEqualTo(-Float.MAX_VALUE);
-    }
-
-    private void waitUntilRankingTimesOut() {
-        while (mAdapter.getHandler().hasMessages(mAdapter.MSG_RANKING_TIMED_OUT)) {
-            try {
-                ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
-                Thread.sleep(100);
-            } catch (InterruptedException e) {
-                // Do nothing
-            }
-        }
+        assertThat(mAdapter.getSearchResults()).containsExactlyElementsIn(results);
+        verify(mFragment).onSearchResultsDisplayed(anyInt());
     }
 
     private List<SearchResult> getDummyDbResults() {
@@ -511,78 +107,21 @@
         ResultPayload payload = new ResultPayload(new Intent());
         SearchResult.Builder builder = new SearchResult.Builder();
         builder.setPayload(payload)
-                .setTitle(TITLES[0])
+                .setTitle("one")
                 .setRank(1)
-                .setStableId(Objects.hash(TITLES[0], "db"));
+                .setStableId(Objects.hash("one", "db"));
         results.add(builder.build());
 
-        builder.setTitle(TITLES[1])
+        builder.setTitle("two")
                 .setRank(3)
-                .setStableId(Objects.hash(TITLES[1], "db"));
+                .setStableId(Objects.hash("two", "db"));
         results.add(builder.build());
 
-        builder.setTitle(TITLES[2])
+        builder.setTitle("three")
                 .setRank(6)
-                .setStableId(Objects.hash(TITLES[2], "db"));
+                .setStableId(Objects.hash("three", "db"));
         results.add(builder.build());
 
         return results;
     }
-
-    private List<SearchResult> getDummyAppResults() {
-        List<SearchResult> results = new ArrayList<>();
-        ResultPayload payload = new ResultPayload(new Intent());
-        AppSearchResult.Builder builder = new AppSearchResult.Builder();
-        builder.setPayload(payload)
-                .setTitle(TITLES[3])
-                .setRank(1)
-                .setStableId(Objects.hash(TITLES[3], "app"));
-        results.add(builder.build());
-
-        builder.setTitle(TITLES[4])
-                .setRank(2)
-                .setStableId(Objects.hash(TITLES[4], "app"));
-        results.add(builder.build());
-
-        builder.setTitle(TITLES[5])
-                .setRank(4)
-                .setStableId(Objects.hash(TITLES[5], "app"));
-        results.add(builder.build());
-
-        return results;
-    }
-
-    private Set<SearchResult> getIntentSampleResults() {
-        Set<SearchResult> sampleResults = new HashSet<>();
-        ArrayList<String> breadcrumbs = new ArrayList<>();
-        final Drawable icon = mContext.getDrawable(R.drawable.ic_search_24dp);
-        final ResultPayload payload = new ResultPayload(null);
-        final SearchResult.Builder builder = new Builder();
-        builder.setTitle("title")
-                .setSummary("summary")
-                .setRank(1)
-                .addBreadcrumbs(breadcrumbs)
-                .setIcon(icon)
-                .setPayload(payload)
-                .setStableId(Objects.hash("title", "summary", 1));
-        sampleResults.add(builder.build());
-
-        builder.setRank(2)
-                .setStableId(Objects.hash("title", "summary", 2));
-        sampleResults.add(builder.build());
-
-        builder.setRank(3)
-                .setStableId(Objects.hash("title", "summary", 3));
-        sampleResults.add(builder.build());
-        return sampleResults;
-    }
-
-    private List<Pair<String, Float>> getDummyRankingScores() {
-        List<SearchResult> results = getDummyDbResults();
-        List<Pair<String, Float>> scores = new ArrayList<>();
-        scores.add(new Pair<>(Long.toString(results.get(2).stableId), 0.9f)); // charlie
-        scores.add(new Pair<>(Long.toString(results.get(0).stableId), 0.8f)); // alpha
-        scores.add(new Pair<>(Long.toString(results.get(1).stableId), 0.2f)); // bravo
-        return scores;
-    }
 }
diff --git a/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java b/tests/robotests/src/com/android/settings/search/StaticSearchResultFutureTaskTest.java
similarity index 61%
rename from tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java
rename to tests/robotests/src/com/android/settings/search/StaticSearchResultFutureTaskTest.java
index dd7b7a9..e285555 100644
--- a/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java
+++ b/tests/robotests/src/com/android/settings/search/StaticSearchResultFutureTaskTest.java
@@ -21,9 +21,11 @@
 import android.content.Context;
 import android.content.Intent;
 import android.database.sqlite.SQLiteDatabase;
+import android.util.Pair;
 
 import com.android.settings.TestConfig;
 import com.android.settings.dashboard.SiteMapManager;
+import com.android.settings.search.DatabaseResultLoader.StaticSearchResultCallable;
 import com.android.settings.search.indexing.IndexData;
 import com.android.settings.testutils.DatabaseTestUtils;
 import com.android.settings.testutils.FakeFeatureFactory;
@@ -39,37 +41,54 @@
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
 
-import java.util.Arrays;
+import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 @RunWith(SettingsRobolectricTestRunner.class)
 @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class DatabaseResultLoaderTest {
+public class StaticSearchResultFutureTaskTest {
 
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
     private Context mMockContext;
     @Mock
     private SiteMapManager mSiteMapManager;
+    @Mock
+    private ExecutorService mService;
     private Context mContext;
 
     SQLiteDatabase mDb;
 
+    FakeFeatureFactory mFeatureFactory;
+
+    private final String[] STATIC_TITLES = {"static one", "static two", "static three"};
+    private final int[] STABLE_IDS =
+            {"id_one".hashCode(), "id_two".hashCode(), "id_three".hashCode()};
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mContext = RuntimeEnvironment.application;
-        FakeFeatureFactory.setupForTest(mMockContext);
-        FakeFeatureFactory factory =
-                (FakeFeatureFactory) FakeFeatureFactory.getFactory(mMockContext);
-        when(factory.searchFeatureProvider.getSiteMapManager())
+        mFeatureFactory = FakeFeatureFactory.setupForTest(mMockContext);
+        when(mFeatureFactory.searchFeatureProvider.getExecutorService()).thenReturn(mService);
+        when(mFeatureFactory.searchFeatureProvider.getSiteMapManager())
                 .thenReturn(mSiteMapManager);
         mDb = IndexDatabaseHelper.getInstance(mContext).getWritableDatabase();
         setUpDb();
@@ -81,159 +100,252 @@
     }
 
     @Test
-    public void testMatchTitle() {
-        DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "title", mSiteMapManager);
-        assertThat(loader.loadInBackground().size()).isEqualTo(2);
+    public void testMatchTitle() throws Exception {
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "title",
+                mSiteMapManager);
+
+        assertThat(loader.call()).hasSize(2);
         verify(mSiteMapManager, times(2)).buildBreadCrumb(eq(mContext), anyString(), anyString());
     }
 
     @Test
-    public void testMatchSummary() {
-        DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "summary",
+    public void testMatchSummary() throws Exception {
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "summary",
                 mSiteMapManager);
-        assertThat(loader.loadInBackground().size()).isEqualTo(2);
+
+        assertThat(loader.call()).hasSize(2);
     }
 
     @Test
-    public void testMatchKeywords() {
-        DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "keywords",
+    public void testMatchKeywords() throws Exception {
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "keywords",
                 mSiteMapManager);
-        assertThat(loader.loadInBackground().size()).isEqualTo(2);
+
+        assertThat(loader.call()).hasSize(2);
     }
 
     @Test
-    public void testMatchEntries() {
-        DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "entries",
+    public void testMatchEntries() throws Exception {
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "entries",
                 mSiteMapManager);
-        assertThat(loader.loadInBackground().size()).isEqualTo(2);
+
+        assertThat(loader.call()).hasSize(2);
     }
 
     @Test
-    public void testSpecialCaseWord_matchesNonPrefix() {
+    public void testSpecialCaseWord_matchesNonPrefix() throws Exception {
         insertSpecialCase("Data usage");
-        DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "usage", mSiteMapManager);
-        assertThat(loader.loadInBackground().size()).isEqualTo(1);
+
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "usage",
+                mSiteMapManager);
+
+        assertThat(loader.call()).hasSize(1);
     }
 
     @Test
-    public void testSpecialCaseDash_matchesWordNoDash() {
+    public void testSpecialCaseDash_matchesWordNoDash() throws Exception {
         insertSpecialCase("wi-fi calling");
-        DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "wifi", mSiteMapManager);
-        assertThat(loader.loadInBackground().size()).isEqualTo(1);
-    }
 
-    @Test
-    public void testSpecialCaseDash_matchesWordWithDash() {
-        insertSpecialCase("priorités seulment");
-        DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "priorités",
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "wifi",
                 mSiteMapManager);
-        assertThat(loader.loadInBackground().size()).isEqualTo(1);
+
+        assertThat(loader.call()).hasSize(1);
     }
 
     @Test
-    public void testSpecialCaseDash_matchesWordWithoutDash() {
+    public void testSpecialCaseDash_matchesWordWithDash() throws Exception {
         insertSpecialCase("priorités seulment");
-        DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "priorites",
+
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "priorités",
                 mSiteMapManager);
-        assertThat(loader.loadInBackground().size()).isEqualTo(1);
+
+        assertThat(loader.call()).hasSize(1);
     }
 
     @Test
-    public void testSpecialCaseDash_matchesEntireQueryWithoutDash() {
+    public void testSpecialCaseDash_matchesWordWithoutDash() throws Exception {
+        insertSpecialCase("priorités seulment");
+
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "priorites",
+                mSiteMapManager);
+
+        assertThat(loader.call()).hasSize(1);
+    }
+
+    @Test
+    public void testSpecialCaseDash_matchesEntireQueryWithoutDash() throws Exception {
         insertSpecialCase("wi-fi calling");
-        DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "wifi calling",
+
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "wifi calling",
                 mSiteMapManager);
-        assertThat(loader.loadInBackground().size()).isEqualTo(1);
+
+        assertThat(loader.call()).hasSize(1);
     }
 
     @Test
-    public void testSpecialCasePrefix_matchesPrefixOfEntry() {
+    public void testSpecialCasePrefix_matchesPrefixOfEntry() throws Exception {
         insertSpecialCase("Photos");
-        DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "pho", mSiteMapManager);
-        assertThat(loader.loadInBackground().size()).isEqualTo(1);
+
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "pho",
+                mSiteMapManager);
+
+        assertThat(loader.call()).hasSize(1);
     }
 
     @Test
-    public void testSpecialCasePrefix_DoesNotMatchNonPrefixSubstring() {
+    public void testSpecialCasePrefix_DoesNotMatchNonPrefixSubstring() throws Exception {
         insertSpecialCase("Photos");
-        DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "hot", mSiteMapManager);
-        assertThat(loader.loadInBackground().size()).isEqualTo(0);
+
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "hot",
+                mSiteMapManager);
+
+        assertThat(loader.call()).hasSize(0);
     }
 
     @Test
-    public void testSpecialCaseMultiWordPrefix_matchesPrefixOfEntry() {
+    public void testSpecialCaseMultiWordPrefix_matchesPrefixOfEntry() throws Exception {
         insertSpecialCase("Apps Notifications");
-        DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "Apps", mSiteMapManager);
-        assertThat(loader.loadInBackground().size()).isEqualTo(1);
+
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "Apps",
+                mSiteMapManager);
+
+        assertThat(loader.call()).hasSize(1);
     }
 
     @Test
-    public void testSpecialCaseMultiWordPrefix_matchesSecondWordPrefixOfEntry() {
+    public void testSpecialCaseMultiWordPrefix_matchesSecondWordPrefixOfEntry() throws Exception {
         insertSpecialCase("Apps Notifications");
-        DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "Not", mSiteMapManager);
-        assertThat(loader.loadInBackground().size()).isEqualTo(1);
+
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "Not",
+                mSiteMapManager);
+
+        assertThat(loader.call()).hasSize(1);
     }
 
     @Test
-    public void testSpecialCaseMultiWordPrefix_DoesNotMatchMatchesPrefixOfFirstEntry() {
+    public void testSpecialCaseMultiWordPrefix_DoesNotMatchMatchesPrefixOfFirstEntry()
+            throws Exception {
         insertSpecialCase("Apps Notifications");
-        DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "pp", mSiteMapManager);
-        assertThat(loader.loadInBackground().size()).isEqualTo(0);
+
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "pp",
+                mSiteMapManager);
+
+        assertThat(loader.call()).hasSize(0);
     }
 
     @Test
-    public void testSpecialCaseMultiWordPrefix_DoesNotMatchMatchesPrefixOfSecondEntry() {
+    public void testSpecialCaseMultiWordPrefix_DoesNotMatchMatchesPrefixOfSecondEntry()
+            throws Exception {
         insertSpecialCase("Apps Notifications");
-        DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "tion", mSiteMapManager);
-        assertThat(loader.loadInBackground().size()).isEqualTo(0);
+
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "tion",
+                mSiteMapManager);
+
+        assertThat(loader.call()).hasSize(0);
     }
 
     @Test
-    public void testSpecialCaseMultiWordPrefixWithSpecial_matchesPrefixOfEntry() {
+    public void testSpecialCaseMultiWordPrefixWithSpecial_matchesPrefixOfEntry() throws
+            Exception {
         insertSpecialCase("Apps & Notifications");
-        DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "App", mSiteMapManager);
-        assertThat(loader.loadInBackground().size()).isEqualTo(1);
+
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "App",
+                mSiteMapManager);
+
+        assertThat(loader.call()).hasSize(1);
     }
 
     @Test
-    public void testSpecialCaseMultiWordPrefixWithSpecial_matchesPrefixOfSecondEntry() {
+    public void testSpecialCaseMultiWordPrefixWithSpecial_matchesPrefixOfSecondEntry()
+            throws Exception {
         insertSpecialCase("Apps & Notifications");
-        DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "No", mSiteMapManager);
-        assertThat(loader.loadInBackground().size()).isEqualTo(1);
+
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "No",
+                mSiteMapManager);
+
+        assertThat(loader.call()).hasSize(1);
     }
 
     @Test
-    public void testResultMatchedByMultipleQueries_duplicatesRemoved() {
+    public void testResultMatchedByMultipleQueries_duplicatesRemoved() throws Exception {
         String key = "durr";
         insertSameValueAllFieldsCase(key);
-        DatabaseResultLoader loader = new DatabaseResultLoader(mContext, key, null);
 
-        assertThat(loader.loadInBackground().size()).isEqualTo(1);
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, key, null);
+
+        assertThat(loader.call()).hasSize(1);
     }
 
     @Test
-    public void testSpecialCaseTwoWords_multipleResults() {
+    public void testSpecialCaseTwoWords_multipleResults() throws Exception {
         final String caseOne = "Apple pear";
         final String caseTwo = "Banana apple";
         insertSpecialCase(caseOne);
         insertSpecialCase(caseTwo);
-        DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "App", null);
-        Set<? extends SearchResult> results = loader.loadInBackground();
-        Set<CharSequence> expectedTitles = new HashSet<>(Arrays.asList(caseOne, caseTwo));
-        Set<CharSequence> actualTitles = new HashSet<>();
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "App", null);
+
+        List<? extends SearchResult> results = loader.call();
+
+        Set<String> actualTitles = new HashSet<>();
         for (SearchResult result : results) {
-            actualTitles.add(result.title);
+            actualTitles.add(result.title.toString());
         }
-        assertThat(actualTitles).isEqualTo(expectedTitles);
+        assertThat(actualTitles).containsAllOf(caseOne, caseTwo);
+    }
+
+    @Test
+    public void testGetRankingScoreByStableId_sortedDynamically() throws Exception {
+        FutureTask<List<Pair<String, Float>>> task = mock(FutureTask.class);
+        when(task.get(anyLong(), any(TimeUnit.class))).thenReturn(getDummyRankingScores());
+        when(mFeatureFactory.searchFeatureProvider.getRankerTask(any(Context.class),
+                anyString())).thenReturn(task);
+        when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(
+                true);
+
+        insertSpecialCase(STATIC_TITLES[0], STABLE_IDS[0]);
+        insertSpecialCase(STATIC_TITLES[1], STABLE_IDS[1]);
+        insertSpecialCase(STATIC_TITLES[2], STABLE_IDS[2]);
+
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "Static",
+                null);
+
+        List<? extends SearchResult> results = loader.call();
+
+        assertThat(results.get(0).title).isEqualTo(STATIC_TITLES[2]);
+        assertThat(results.get(1).title).isEqualTo(STATIC_TITLES[0]);
+        assertThat(results.get(2).title).isEqualTo(STATIC_TITLES[1]);
+    }
+
+    @Test
+    public void testGetRankingScoreByStableId_scoresTimeout_sortedStatically() throws Exception {
+        Callable<List<Pair<String, Float>>> callable = mock(Callable.class);
+        when(callable.call()).thenThrow(new TimeoutException());
+        FutureTask<List<Pair<String, Float>>> task = new FutureTask<>(callable);
+        when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(
+                true);
+        when(mFeatureFactory.searchFeatureProvider.getRankerTask(any(Context.class),
+                anyString())).thenReturn(task);
+        insertSpecialCase("title", STABLE_IDS[0]);
+
+        StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "title", null);
+
+        List<? extends SearchResult> results = loader.call();
+        assertThat(results.get(0).title).isEqualTo("title");
+        assertThat(results.get(1).title).isEqualTo("alpha_title");
+        assertThat(results.get(2).title).isEqualTo("bravo_title");
     }
 
     private void insertSpecialCase(String specialCase) {
+        insertSpecialCase(specialCase, specialCase.hashCode());
+    }
+
+    private void insertSpecialCase(String specialCase, int docId) {
         String normalized = IndexData.normalizeHyphen(specialCase);
         normalized = IndexData.normalizeString(normalized);
         final ResultPayload payload = new ResultPayload(new Intent());
 
         ContentValues values = new ContentValues();
-        values.put(IndexDatabaseHelper.IndexColumns.DOCID, normalized.hashCode());
+        values.put(IndexDatabaseHelper.IndexColumns.DOCID, docId);
         values.put(IndexDatabaseHelper.IndexColumns.LOCALE, "en-us");
         values.put(IndexDatabaseHelper.IndexColumns.DATA_RANK, 1);
         values.put(IndexDatabaseHelper.IndexColumns.DATA_TITLE, specialCase);
@@ -373,4 +485,33 @@
 
         mDb.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX, null, values);
     }
+
+    private List<? extends SearchResult> getDummyDbResults() {
+        List<SearchResult> results = new ArrayList<>();
+        ResultPayload payload = new ResultPayload(new Intent());
+        SearchResult.Builder builder = new SearchResult.Builder();
+        builder.setPayload(payload)
+                .setTitle(STATIC_TITLES[0])
+                .setStableId(STABLE_IDS[0]);
+        results.add(builder.build());
+
+        builder.setTitle(STATIC_TITLES[1])
+                .setStableId(STABLE_IDS[1]);
+        results.add(builder.build());
+
+        builder.setTitle(STATIC_TITLES[2])
+                .setStableId(STABLE_IDS[2]);
+        results.add(builder.build());
+
+        return results;
+    }
+
+    private List<Pair<String, Float>> getDummyRankingScores() {
+        List<? extends SearchResult> results = getDummyDbResults();
+        List<Pair<String, Float>> scores = new ArrayList<>();
+        scores.add(new Pair<>(Long.toString(results.get(2).stableId), 0.9f)); // static_three
+        scores.add(new Pair<>(Long.toString(results.get(0).stableId), 0.8f)); // static_one
+        scores.add(new Pair<>(Long.toString(results.get(1).stableId), 0.2f)); // static_two
+        return scores;
+    }
 }
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/EncryptionAndCredentialTest.java b/tests/robotests/src/com/android/settings/security/EncryptionAndCredentialTest.java
similarity index 95%
rename from tests/robotests/src/com/android/settings/EncryptionAndCredentialTest.java
rename to tests/robotests/src/com/android/settings/security/EncryptionAndCredentialTest.java
index 7a3875e..c87c964 100644
--- a/tests/robotests/src/com/android/settings/EncryptionAndCredentialTest.java
+++ b/tests/robotests/src/com/android/settings/security/EncryptionAndCredentialTest.java
@@ -11,14 +11,14 @@
  * 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
+ * limitations under the License.
  */
 
-package com.android.settings;
+package com.android.settings.security;
 
 import static android.app.admin.DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE;
 import static android.app.admin.DevicePolicyManager.ENCRYPTION_STATUS_INACTIVE;
-import static com.android.settings.EncryptionAndCredential.SEARCH_INDEX_DATA_PROVIDER;
+import static com.android.settings.security.EncryptionAndCredential.SEARCH_INDEX_DATA_PROVIDER;
 import static com.google.common.truth.Truth.assertThat;
 import static org.mockito.Mockito.when;
 
@@ -28,6 +28,8 @@
 import android.provider.SearchIndexableResource;
 
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.settings.R;
+import com.android.settings.TestConfig;
 import com.android.settings.search.BaseSearchIndexProvider;
 import com.android.settings.testutils.SettingsRobolectricTestRunner;
 
diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowVibrator.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowVibrator.java
new file mode 100644
index 0000000..9046720
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowVibrator.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.testutils.shadow;
+
+import static org.mockito.Mockito.mock;
+
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.os.SystemVibrator;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.fakes.RoboVibrator;
+import org.robolectric.shadows.ShadowContextImpl;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.Map;
+
+@Implements(SystemVibrator.class)
+public class ShadowVibrator {
+
+    private static Map<String, String> getSystemServiceMap() {
+        return ReflectionHelpers.getStaticField(ShadowContextImpl.class, "SYSTEM_SERVICE_MAP");
+    }
+
+    public static void addToServiceMap() {
+        getSystemServiceMap().put(Context.VIBRATOR_SERVICE, SystemVibrator.class.getName());
+    }
+
+    public static void reset() {
+        getSystemServiceMap().put(Context.VIBRATOR_SERVICE, RoboVibrator.class.getName());
+    }
+
+    public final Vibrator delegate = mock(Vibrator.class);
+
+    @Implementation
+    public void vibrate(int uid, String opPkg, VibrationEffect vibe, AudioAttributes attributes) {
+        delegate.vibrate(uid, opPkg, vibe, attributes);
+    }
+}
diff --git a/tests/unit/Android.mk b/tests/unit/Android.mk
index ec0f0b6..58fe7dd 100644
--- a/tests/unit/Android.mk
+++ b/tests/unit/Android.mk
@@ -10,13 +10,13 @@
 LOCAL_STATIC_JAVA_LIBRARIES := \
     android-support-test \
     espresso-core \
-    legacy-android-test \
-    mockito-target-minus-junit4 \
-    truth-prebuilt \
-    ub-uiautomator \
     espresso-contrib-nodep \
     espresso-intents-nodep \
-
+    legacy-android-test \
+    mockito-target-minus-junit4 \
+    platform-test-annotations \
+    truth-prebuilt \
+    ub-uiautomator
 
 # Include all test java files.
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
diff --git a/tests/unit/src/com/android/settings/core/PreferenceControllerContractTest.java b/tests/unit/src/com/android/settings/core/PreferenceControllerContractTest.java
new file mode 100644
index 0000000..c75ca13
--- /dev/null
+++ b/tests/unit/src/com/android/settings/core/PreferenceControllerContractTest.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.core;
+
+import static junit.framework.Assert.fail;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+import android.provider.SearchIndexableResource;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.ArraySet;
+
+import com.android.settings.search.DatabaseIndexingUtils;
+import com.android.settings.search.Indexable;
+import com.android.settings.search.SearchIndexableResources;
+import com.android.settingslib.core.AbstractPreferenceController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class PreferenceControllerContractTest {
+
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getTargetContext();
+    }
+
+    @Test
+    @Presubmit
+    public void controllersInSearchShouldImplementPreferenceControllerMixin() {
+        final Set<String> errorClasses = new ArraySet<>();
+
+        for (SearchIndexableResource page : SearchIndexableResources.values()) {
+            final Class<?> clazz = DatabaseIndexingUtils.getIndexableClass(page.className);
+
+            final Indexable.SearchIndexProvider provider =
+                    DatabaseIndexingUtils.getSearchIndexProvider(clazz);
+            if (provider == null) {
+                continue;
+            }
+
+            final List<AbstractPreferenceController> controllers =
+                    provider.getPreferenceControllers(mContext);
+            if (controllers == null) {
+                continue;
+            }
+            for (AbstractPreferenceController controller : controllers) {
+                if (!(controller instanceof PreferenceControllerMixin)) {
+                    errorClasses.add(controller.getClass().getName());
+                }
+            }
+        }
+
+        if (!errorClasses.isEmpty()) {
+            final StringBuilder errorMessage = new StringBuilder()
+                    .append("Each preference must implement PreferenceControllerMixin, ")
+                    .append("the following classes don't:\n");
+            for (String c : errorClasses) {
+                errorMessage.append(c).append("\n");
+            }
+            fail(errorMessage.toString());
+        }
+    }
+}
diff --git a/tests/unit/src/com/android/settings/UniquePreferenceTest.java b/tests/unit/src/com/android/settings/core/UniquePreferenceTest.java
similarity index 98%
rename from tests/unit/src/com/android/settings/UniquePreferenceTest.java
rename to tests/unit/src/com/android/settings/core/UniquePreferenceTest.java
index 74f46eb..4c51772 100644
--- a/tests/unit/src/com/android/settings/UniquePreferenceTest.java
+++ b/tests/unit/src/com/android/settings/core/UniquePreferenceTest.java
@@ -14,13 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.settings;
+package com.android.settings.core;
 
 import static junit.framework.Assert.fail;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
+import android.platform.test.annotations.Presubmit;
 import android.provider.SearchIndexableResource;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.MediumTest;
@@ -83,6 +84,7 @@
      * should have a key.
      */
     @Test
+    @Presubmit
     public void allPreferencesShouldHaveUniqueKey()
             throws IOException, XmlPullParserException, Resources.NotFoundException {
         final Set<String> uniqueKeys = new HashSet<>();