Merge "Add Wi-Fi quality badge icons."
diff --git a/api/system-current.txt b/api/system-current.txt
index 8bf880b..d84a31e 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -25673,7 +25673,7 @@
     field public static final java.lang.String EXTRA_SEQUENCE = "android.net.extra.SEQUENCE";
   }
 
-  public static final class NetworkRecommendationProvider.ResultCallback {
+  public static class NetworkRecommendationProvider.ResultCallback {
     method public void onResult(android.net.RecommendationResult);
   }
 
diff --git a/core/java/android/net/NetworkRecommendationProvider.java b/core/java/android/net/NetworkRecommendationProvider.java
index af5a052c..16ae867 100644
--- a/core/java/android/net/NetworkRecommendationProvider.java
+++ b/core/java/android/net/NetworkRecommendationProvider.java
@@ -75,7 +75,7 @@
      * A callback implementing applications should invoke when a {@link RecommendationResult}
      * is available.
      */
-    public static final class ResultCallback {
+    public static class ResultCallback {
         private final IRemoteCallback mCallback;
         private final int mSequence;
         private final AtomicBoolean mCallbackRun;
diff --git a/core/java/android/net/NetworkScoreManager.java b/core/java/android/net/NetworkScoreManager.java
index e08767c..1825956 100644
--- a/core/java/android/net/NetworkScoreManager.java
+++ b/core/java/android/net/NetworkScoreManager.java
@@ -183,7 +183,7 @@
         if (app == null) {
             return null;
         }
-        return app.mPackageName;
+        return app.packageName;
     }
 
     /**
@@ -272,19 +272,11 @@
      * @hide
      */
     public boolean requestScores(NetworkKey[] networks) throws SecurityException {
-        String activeScorer = getActiveScorerPackage();
-        if (activeScorer == null) {
-            return false;
+        try {
+            return mService.requestScores(networks);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
         }
-        Intent intent = new Intent(ACTION_SCORE_NETWORKS);
-        intent.setPackage(activeScorer);
-        intent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
-        intent.putExtra(EXTRA_NETWORKS_TO_SCORE, networks);
-        // A scorer should never become active if its package doesn't hold SCORE_NETWORKS, but
-        // ensure the package still holds it to be extra safe.
-        // TODO: http://b/23422763
-        mContext.sendBroadcastAsUser(intent, UserHandle.SYSTEM, Manifest.permission.SCORE_NETWORKS);
-        return true;
     }
 
     /**
@@ -344,6 +336,8 @@
     /**
      * Request a recommendation for which network to connect to.
      *
+     * <p>It is not safe to call this method from the main thread.
+     *
      * @param request a {@link RecommendationRequest} instance containing additional
      *                request details
      * @return a {@link RecommendationResult} instance containing the recommended network
diff --git a/core/java/android/net/NetworkScorerAppManager.java b/core/java/android/net/NetworkScorerAppManager.java
index ebb31c9..4282ca7 100644
--- a/core/java/android/net/NetworkScorerAppManager.java
+++ b/core/java/android/net/NetworkScorerAppManager.java
@@ -19,160 +19,176 @@
 import android.Manifest;
 import android.Manifest.permission;
 import android.annotation.Nullable;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.Log;
-
+import com.android.internal.R;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 
 /**
- * Internal class for managing the primary network scorer application.
- *
- * TODO: Rename this to something more generic.
+ * Internal class for discovering and managing the network scorer/recommendation application.
  *
  * @hide
  */
 public class NetworkScorerAppManager {
     private static final String TAG = "NetworkScorerAppManager";
-
-    private static final Intent SCORE_INTENT =
-            new Intent(NetworkScoreManager.ACTION_SCORE_NETWORKS);
-
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
     private final Context mContext;
 
     public NetworkScorerAppManager(Context context) {
       mContext = context;
     }
 
+    /**
+     * Holds metadata about a discovered network scorer/recommendation application.
+     */
     public static class NetworkScorerAppData {
         /** Package name of this scorer app. */
-        public final String mPackageName;
+        public final String packageName;
 
         /** UID of the scorer app. */
-        public final int mPackageUid;
-
-        /** Name of this scorer app for display. */
-        public final CharSequence mScorerName;
+        public final int packageUid;
 
         /**
-         * Optional class name of a configuration activity. Null if none is set.
-         *
-         * @see NetworkScoreManager#ACTION_CUSTOM_ENABLE
+         * Name of the recommendation service we can bind to.
          */
-        public final String mConfigurationActivityClassName;
+        public final String recommendationServiceClassName;
 
-        /**
-         * Optional class name of the scoring service we can bind to. Null if none is set.
-         */
-        public final String mScoringServiceClassName;
-
-        public NetworkScorerAppData(String packageName, int packageUid, CharSequence scorerName,
-                @Nullable String configurationActivityClassName,
-                @Nullable String scoringServiceClassName) {
-            mScorerName = scorerName;
-            mPackageName = packageName;
-            mPackageUid = packageUid;
-            mConfigurationActivityClassName = configurationActivityClassName;
-            mScoringServiceClassName = scoringServiceClassName;
+        public NetworkScorerAppData(String packageName, int packageUid,
+                String recommendationServiceClassName) {
+            this.packageName = packageName;
+            this.packageUid = packageUid;
+            this.recommendationServiceClassName = recommendationServiceClassName;
         }
 
         @Override
         public String toString() {
             final StringBuilder sb = new StringBuilder("NetworkScorerAppData{");
-            sb.append("mPackageName='").append(mPackageName).append('\'');
-            sb.append(", mPackageUid=").append(mPackageUid);
-            sb.append(", mScorerName=").append(mScorerName);
-            sb.append(", mConfigurationActivityClassName='").append(mConfigurationActivityClassName)
-                    .append('\'');
-            sb.append(", mScoringServiceClassName='").append(mScoringServiceClassName).append('\'');
+            sb.append("mPackageName='").append(packageName).append('\'');
+            sb.append(", packageUid=").append(packageUid);
+            sb.append(", recommendationServiceClassName='")
+                    .append(recommendationServiceClassName).append('\'');
             sb.append('}');
             return sb.toString();
         }
     }
 
     /**
-     * Returns the list of available scorer apps.
+     * @return A {@link NetworkScorerAppData} instance containing information about the
+     *         best configured network recommendation provider installed or {@code null}
+     *         if none of the configured packages can recommend networks.
      *
-     * <p>A network scorer is any application which:
+     * <p>A network recommendation provider is any application which:
      * <ul>
+     * <li>Is listed in the <code>config_networkRecommendationPackageNames</code> config.
      * <li>Declares the {@link android.Manifest.permission#SCORE_NETWORKS} permission.
-     * <li>Includes a receiver for {@link NetworkScoreManager#ACTION_SCORE_NETWORKS} guarded by the
-     *     {@link android.Manifest.permission#BROADCAST_NETWORK_PRIVILEGED} permission.
+     * <li>Includes a Service for {@link NetworkScoreManager#ACTION_RECOMMEND_NETWORKS}.
      * </ul>
-     *
-     * @return the list of scorers, or the empty list if there are no valid scorers.
      */
-    public Collection<NetworkScorerAppData> getAllValidScorers() {
-        // Network scorer apps can only run as the primary user so exit early if we're not the
-        // primary user.
+    public NetworkScorerAppData getNetworkRecommendationProviderData() {
+        // Network recommendation apps can only run as the primary user right now.
+        // http://b/23422763
         if (UserHandle.getCallingUserId() != UserHandle.USER_SYSTEM) {
+            return null;
+        }
+
+        final List<String> potentialPkgs = getPotentialRecommendationProviderPackages();
+        if (potentialPkgs.isEmpty()) {
+            if (DEBUG) {
+                Log.d(TAG, "No Network Recommendation Providers specified.");
+            }
+            return null;
+        }
+
+        final PackageManager pm = mContext.getPackageManager();
+        for (int i = 0; i < potentialPkgs.size(); i++) {
+            final String potentialPkg = potentialPkgs.get(i);
+
+            // Look for the recommendation service class and required receiver.
+            final ResolveInfo resolveServiceInfo = findRecommendationService(potentialPkg);
+            if (resolveServiceInfo != null) {
+                return new NetworkScorerAppData(potentialPkg,
+                    resolveServiceInfo.serviceInfo.applicationInfo.uid,
+                    resolveServiceInfo.serviceInfo.name);
+            } else {
+                if (DEBUG) {
+                    Log.d(TAG, potentialPkg + " does not have the required components, skipping.");
+                }
+            }
+        }
+
+        // None of the configured packages are valid.
+        return null;
+    }
+
+    /**
+     * @return A priority order list of package names that have been granted the
+     *         permission needed for them to act as a network recommendation provider.
+     *         The packages in the returned list may not contain the other required
+     *         network recommendation provider components so additional checks are required
+     *         before making a package the network recommendation provider.
+     */
+    public List<String> getPotentialRecommendationProviderPackages() {
+        final String[] packageArray = mContext.getResources().getStringArray(
+                R.array.config_networkRecommendationPackageNames);
+        if (packageArray == null || packageArray.length == 0) {
+            if (DEBUG) {
+                Log.d(TAG, "No Network Recommendation Providers specified.");
+            }
             return Collections.emptyList();
         }
 
-        List<NetworkScorerAppData> scorers = new ArrayList<>();
-        PackageManager pm = mContext.getPackageManager();
-        // Only apps installed under the primary user of the device can be scorers.
-        // TODO: http://b/23422763
-        List<ResolveInfo> receivers =
-                pm.queryBroadcastReceiversAsUser(SCORE_INTENT, 0 /* flags */, UserHandle.USER_SYSTEM);
-        for (ResolveInfo receiver : receivers) {
-            // This field is a misnomer, see android.content.pm.ResolveInfo#activityInfo
-            final ActivityInfo receiverInfo = receiver.activityInfo;
-            if (receiverInfo == null) {
-                // Should never happen with queryBroadcastReceivers, but invalid nonetheless.
-                continue;
-            }
-            if (!permission.BROADCAST_NETWORK_PRIVILEGED.equals(receiverInfo.permission)) {
-                // Receiver doesn't require the BROADCAST_NETWORK_PRIVILEGED permission, which
-                // means anyone could trigger network scoring and flood the framework with score
-                // requests.
-                continue;
-            }
-            if (pm.checkPermission(permission.SCORE_NETWORKS, receiverInfo.packageName) !=
-                    PackageManager.PERMISSION_GRANTED) {
-                // Application doesn't hold the SCORE_NETWORKS permission, so the user never
-                // approved it as a network scorer.
-                continue;
-            }
-
-            // Optionally, this package may specify a configuration activity.
-            String configurationActivityClassName = null;
-            Intent intent = new Intent(NetworkScoreManager.ACTION_CUSTOM_ENABLE);
-            intent.setPackage(receiverInfo.packageName);
-            List<ResolveInfo> configActivities = pm.queryIntentActivities(intent, 0 /* flags */);
-            if (configActivities != null && !configActivities.isEmpty()) {
-                ActivityInfo activityInfo = configActivities.get(0).activityInfo;
-                if (activityInfo != null) {
-                    configurationActivityClassName = activityInfo.name;
-                }
-            }
-
-            // Find the scoring service class we can bind to, if any.
-            String scoringServiceClassName = null;
-            Intent serviceIntent = new Intent(NetworkScoreManager.ACTION_SCORE_NETWORKS);
-            serviceIntent.setPackage(receiverInfo.packageName);
-            ResolveInfo resolveServiceInfo = pm.resolveService(serviceIntent, 0 /* flags */);
-            if (resolveServiceInfo != null && resolveServiceInfo.serviceInfo != null) {
-                scoringServiceClassName = resolveServiceInfo.serviceInfo.name;
-            }
-
-            // NOTE: loadLabel will attempt to load the receiver's label and fall back to the
-            // app label if none is present.
-            scorers.add(new NetworkScorerAppData(receiverInfo.packageName,
-                    receiverInfo.applicationInfo.uid, receiverInfo.loadLabel(pm),
-                    configurationActivityClassName, scoringServiceClassName));
+        if (VERBOSE) {
+            Log.d(TAG, "Configured packages: " + TextUtils.join(", ", packageArray));
         }
 
-        return scorers;
+        List<String> packages = new ArrayList<>();
+        final PackageManager pm = mContext.getPackageManager();
+        for (String potentialPkg : packageArray) {
+            if (pm.checkPermission(permission.SCORE_NETWORKS, potentialPkg)
+                    == PackageManager.PERMISSION_GRANTED) {
+                packages.add(potentialPkg);
+            } else {
+                if (DEBUG) {
+                    Log.d(TAG, potentialPkg + " has not been granted " + permission.SCORE_NETWORKS
+                            + ", skipping.");
+                }
+            }
+        }
+
+        return packages;
+    }
+
+    private ResolveInfo findRecommendationService(String packageName) {
+        final PackageManager pm = mContext.getPackageManager();
+        final int resolveFlags = 0;
+
+        final Intent serviceIntent = new Intent(NetworkScoreManager.ACTION_RECOMMEND_NETWORKS);
+        serviceIntent.setPackage(packageName);
+        final ResolveInfo resolveServiceInfo =
+                pm.resolveService(serviceIntent, resolveFlags);
+
+        if (VERBOSE) {
+            Log.d(TAG, "Resolved " + serviceIntent + " to " + resolveServiceInfo);
+        }
+
+        if (resolveServiceInfo != null && resolveServiceInfo.serviceInfo != null) {
+            return resolveServiceInfo;
+        }
+
+        if (VERBOSE) {
+            Log.v(TAG, packageName + " does not have a service for " + serviceIntent);
+        }
+        return null;
     }
 
     /**
@@ -182,10 +198,15 @@
      *     selected) or if the previously-set scorer is no longer a valid scorer app (e.g. because
      *     it was disabled or uninstalled).
      */
+    @Nullable
     public NetworkScorerAppData getActiveScorer() {
-        String scorerPackage = Settings.Global.getString(mContext.getContentResolver(),
-                Settings.Global.NETWORK_SCORER_APP);
-        return getScorer(scorerPackage);
+        if (isNetworkRecommendationsDisabled()) {
+            // If recommendations are disabled then there can't be an active scorer.
+            return null;
+        }
+
+        // Otherwise return the recommendation provider (which may be null).
+        return getNetworkRecommendationProviderData();
     }
 
     /**
@@ -195,33 +216,13 @@
      *
      * @param packageName the packageName of the new scorer to use. If null, scoring will be
      *     disabled. Otherwise, the scorer will only be set if it is a valid scorer application.
-     * @return true if the scorer was changed, or false if the package is not a valid scorer.
+     * @return true if the scorer was changed, or false if the package is not a valid scorer or
+     *         a valid network recommendation provider exists.
+     * @deprecated Scorers are now selected from a configured list.
      */
+    @Deprecated
     public boolean setActiveScorer(String packageName) {
-        String oldPackageName = Settings.Global.getString(mContext.getContentResolver(),
-                Settings.Global.NETWORK_SCORER_APP);
-        if (TextUtils.equals(oldPackageName, packageName)) {
-            // No change.
-            return true;
-        }
-
-        Log.i(TAG, "Changing network scorer from " + oldPackageName + " to " + packageName);
-
-        if (packageName == null) {
-            Settings.Global.putString(mContext.getContentResolver(),
-                    Settings.Global.NETWORK_SCORER_APP, null);
-            return true;
-        } else {
-            // We only make the change if the new package is valid.
-            if (getScorer(packageName) != null) {
-                Settings.Global.putString(mContext.getContentResolver(),
-                        Settings.Global.NETWORK_SCORER_APP, packageName);
-                return true;
-            } else {
-                Log.w(TAG, "Requested network scorer is not valid: " + packageName);
-                return false;
-            }
-        }
+        return false;
     }
 
     /** Determine whether the application with the given UID is the enabled scorer. */
@@ -230,7 +231,7 @@
         if (defaultApp == null) {
             return false;
         }
-        if (callingUid != defaultApp.mPackageUid) {
+        if (callingUid != defaultApp.packageUid) {
             return false;
         }
         // To be extra safe, ensure the caller holds the SCORE_NETWORKS permission. It always
@@ -239,17 +240,9 @@
                 PackageManager.PERMISSION_GRANTED;
     }
 
-    /** Returns the {@link NetworkScorerAppData} for the given app, or null if it's not a scorer. */
-    public NetworkScorerAppData getScorer(String packageName) {
-        if (TextUtils.isEmpty(packageName)) {
-            return null;
-        }
-        Collection<NetworkScorerAppData> applications = getAllValidScorers();
-        for (NetworkScorerAppData app : applications) {
-            if (packageName.equals(app.mPackageName)) {
-                return app;
-            }
-        }
-        return null;
+    private boolean isNetworkRecommendationsDisabled() {
+        final ContentResolver cr = mContext.getContentResolver();
+        // A value of 1 indicates enabled.
+        return Settings.Global.getInt(cr, Settings.Global.NETWORK_RECOMMENDATIONS_ENABLED, 0) != 1;
     }
 }
diff --git a/core/java/android/net/RecommendationRequest.java b/core/java/android/net/RecommendationRequest.java
index 05ca1aa..a96f90d 100644
--- a/core/java/android/net/RecommendationRequest.java
+++ b/core/java/android/net/RecommendationRequest.java
@@ -105,7 +105,16 @@
     }
 
     protected RecommendationRequest(Parcel in) {
-        mScanResults = (ScanResult[]) in.readParcelableArray(ScanResult.class.getClassLoader());
+        final int resultCount = in.readInt();
+        if (resultCount > 0) {
+            mScanResults = new ScanResult[resultCount];
+            for (int i = 0; i < resultCount; i++) {
+                mScanResults[i] = in.readParcelable(ScanResult.class.getClassLoader());
+            }
+        } else {
+            mScanResults = null;
+        }
+
         mCurrentSelectedConfig = in.readParcelable(WifiConfiguration.class.getClassLoader());
         mRequiredCapabilities = in.readParcelable(NetworkCapabilities.class.getClassLoader());
     }
@@ -117,7 +126,14 @@
 
     @Override
     public void writeToParcel(Parcel dest, int flags) {
-        dest.writeParcelableArray(mScanResults, flags);
+        if (mScanResults != null) {
+            dest.writeInt(mScanResults.length);
+            for (int i = 0; i < mScanResults.length; i++) {
+                dest.writeParcelable(mScanResults[i], flags);
+            }
+        } else {
+            dest.writeInt(0);
+        }
         dest.writeParcelable(mCurrentSelectedConfig, flags);
         dest.writeParcelable(mRequiredCapabilities, flags);
     }
diff --git a/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java b/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java
index 78d3b7b..0216a07 100644
--- a/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java
+++ b/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java
@@ -579,7 +579,7 @@
             throws SignatureNotFoundException {
         // Look up the offset of ZIP Central Directory.
         long centralDirOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocd);
-        if (centralDirOffset >= eocdOffset) {
+        if (centralDirOffset > eocdOffset) {
             throw new SignatureNotFoundException(
                     "ZIP Central Directory offset out of range: " + centralDirOffset
                     + ". ZIP End of Central Directory offset: " + eocdOffset);
diff --git a/core/java/android/util/apk/ZipUtils.java b/core/java/android/util/apk/ZipUtils.java
index cdbac18..fa5477e 100644
--- a/core/java/android/util/apk/ZipUtils.java
+++ b/core/java/android/util/apk/ZipUtils.java
@@ -160,7 +160,7 @@
         }
         int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE);
         int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE;
-        for (int expectedCommentLength = 0; expectedCommentLength < maxCommentLength;
+        for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength;
                 expectedCommentLength++) {
             int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength;
             if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) {
diff --git a/core/tests/coretests/src/android/net/NetworkScorerAppManagerTest.java b/core/tests/coretests/src/android/net/NetworkScorerAppManagerTest.java
index 02c2517..5bfff26 100644
--- a/core/tests/coretests/src/android/net/NetworkScorerAppManagerTest.java
+++ b/core/tests/coretests/src/android/net/NetworkScorerAppManagerTest.java
@@ -16,32 +16,33 @@
 
 package android.net;
 
+import static org.mockito.Mockito.when;
+
 import android.Manifest.permission;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
 import android.net.NetworkScorerAppManager.NetworkScorerAppData;
-import android.os.UserHandle;
+import android.provider.Settings;
 import android.test.InstrumentationTestCase;
-
+import com.android.internal.R;
+import java.util.List;
 import org.mockito.ArgumentMatcher;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-
 public class NetworkScorerAppManagerTest extends InstrumentationTestCase {
     @Mock private Context mMockContext;
     @Mock private PackageManager mMockPm;
-
+    @Mock private Resources mResources;
+    @Mock private ContentResolver mContentResolver;
+    private Context mTargetContext;
     private NetworkScorerAppManager mNetworkScorerAppManager;
 
     @Override
@@ -49,154 +50,161 @@
         super.setUp();
 
         // Configuration needed to make mockito/dexcache work.
-        System.setProperty("dexmaker.dexcache",
-                getInstrumentation().getTargetContext().getCacheDir().getPath());
+        mTargetContext = getInstrumentation().getTargetContext();
+        System.setProperty("dexmaker.dexcache", mTargetContext.getCacheDir().getPath());
         ClassLoader newClassLoader = getInstrumentation().getClass().getClassLoader();
         Thread.currentThread().setContextClassLoader(newClassLoader);
 
         MockitoAnnotations.initMocks(this);
-        Mockito.when(mMockContext.getPackageManager()).thenReturn(mMockPm);
+        when(mMockContext.getPackageManager()).thenReturn(mMockPm);
+        when(mMockContext.getResources()).thenReturn(mResources);
+        when(mMockContext.getContentResolver()).thenReturn(mTargetContext.getContentResolver());
         mNetworkScorerAppManager = new NetworkScorerAppManager(mMockContext);
     }
 
-    public void testGetAllValidScorers() throws Exception {
-        // Package 1 - Valid scorer.
-        ResolveInfoHolder package1 = buildResolveInfo("package1", 1, true, true, false, false);
-
-        // Package 2 - Receiver does not have BROADCAST_NETWORK_PRIVILEGED permission.
-        ResolveInfoHolder package2 = buildResolveInfo("package2", 2, false, true, false, false);
-
-        // Package 3 - App does not have SCORE_NETWORKS permission.
-        ResolveInfoHolder package3 = buildResolveInfo("package3", 3, true, false, false, false);
-
-        // Package 4 - Valid scorer w/ optional config activity.
-        ResolveInfoHolder package4 = buildResolveInfo("package4", 4, true, true, true, false);
-
-        // Package 5 - Valid scorer w/ optional service to bind to.
-        ResolveInfoHolder package5 = buildResolveInfo("package5", 5, true, true, false, true);
-
-        List<ResolveInfoHolder> scorers = new ArrayList<>();
-        scorers.add(package1);
-        scorers.add(package2);
-        scorers.add(package3);
-        scorers.add(package4);
-        scorers.add(package5);
-        setScorers(scorers);
-
-        Iterator<NetworkScorerAppData> result =
-                mNetworkScorerAppManager.getAllValidScorers().iterator();
-
-        assertTrue(result.hasNext());
-        NetworkScorerAppData next = result.next();
-        assertEquals("package1", next.mPackageName);
-        assertEquals(1, next.mPackageUid);
-        assertNull(next.mConfigurationActivityClassName);
-
-        assertTrue(result.hasNext());
-        next = result.next();
-        assertEquals("package4", next.mPackageName);
-        assertEquals(4, next.mPackageUid);
-        assertEquals(".ConfigActivity", next.mConfigurationActivityClassName);
-
-        assertTrue(result.hasNext());
-        next = result.next();
-        assertEquals("package5", next.mPackageName);
-        assertEquals(5, next.mPackageUid);
-        assertEquals(".ScoringService", next.mScoringServiceClassName);
-
-        assertFalse(result.hasNext());
+    public void testGetPotentialRecommendationProviderPackages_emptyConfig() throws Exception {
+        setNetworkRecommendationPackageNames(/*no configured packages*/);
+        assertTrue(mNetworkScorerAppManager.getPotentialRecommendationProviderPackages().isEmpty());
     }
 
-    private void setScorers(List<ResolveInfoHolder> scorers) {
-        List<ResolveInfo> receivers = new ArrayList<>();
-        for (final ResolveInfoHolder scorer : scorers) {
-            receivers.add(scorer.scorerResolveInfo);
-            if (scorer.configActivityResolveInfo != null) {
-                // This scorer has a config activity.
-                Mockito.when(mMockPm.queryIntentActivities(
-                        Mockito.argThat(new ArgumentMatcher<Intent>() {
-                            @Override
-                            public boolean matches(Object object) {
-                                Intent intent = (Intent) object;
-                                return NetworkScoreManager.ACTION_CUSTOM_ENABLE.equals(
-                                        intent.getAction())
-                                        && scorer.scorerResolveInfo.activityInfo.packageName.equals(
-                                                intent.getPackage());
-                            }
-                        }), Mockito.eq(0))).thenReturn(
-                                Collections.singletonList(scorer.configActivityResolveInfo));
-            }
+    public void testGetPotentialRecommendationProviderPackages_permissionNotGranted()
+            throws Exception {
+        setNetworkRecommendationPackageNames("package1");
+        mockScoreNetworksDenied("package1");
 
-            if (scorer.serviceResolveInfo != null) {
-                // This scorer has a service to bind to
-                Mockito.when(mMockPm.resolveService(
-                        Mockito.argThat(new ArgumentMatcher<Intent>() {
-                            @Override
-                            public boolean matches(Object object) {
-                                Intent intent = (Intent) object;
-                                return NetworkScoreManager.ACTION_SCORE_NETWORKS.equals(
-                                        intent.getAction())
-                                        && scorer.scorerResolveInfo.activityInfo.packageName.equals(
-                                        intent.getPackage());
-                            }
-                        }), Mockito.eq(0))).thenReturn(scorer.serviceResolveInfo);
-            }
+        assertTrue(mNetworkScorerAppManager.getPotentialRecommendationProviderPackages().isEmpty());
+    }
+
+    public void testGetPotentialRecommendationProviderPackages_permissionGranted()
+            throws Exception {
+        setNetworkRecommendationPackageNames("package1");
+        mockScoreNetworksGranted("package1");
+
+        List<String> potentialProviderPackages =
+                mNetworkScorerAppManager.getPotentialRecommendationProviderPackages();
+
+        assertFalse(potentialProviderPackages.isEmpty());
+        assertEquals("package1", potentialProviderPackages.get(0));
+    }
+
+    public void testGetPotentialRecommendationProviderPackages_multipleConfigured()
+            throws Exception {
+        setNetworkRecommendationPackageNames("package1", "package2");
+        mockScoreNetworksDenied("package1");
+        mockScoreNetworksGranted("package2");
+
+        List<String> potentialProviderPackages =
+                mNetworkScorerAppManager.getPotentialRecommendationProviderPackages();
+
+        assertEquals(1, potentialProviderPackages.size());
+        assertEquals("package2", potentialProviderPackages.get(0));
+    }
+
+    public void testGetNetworkRecommendationProviderData_noPotentialPackages() throws Exception {
+        setNetworkRecommendationPackageNames(/*no configured packages*/);
+        assertNull(mNetworkScorerAppManager.getNetworkRecommendationProviderData());
+    }
+
+    public void testGetNetworkRecommendationProviderData_serviceMissing() throws Exception {
+        setNetworkRecommendationPackageNames("package1");
+        mockScoreNetworksGranted("package1");
+
+        assertNull(mNetworkScorerAppManager.getNetworkRecommendationProviderData());
+    }
+
+    public void testGetNetworkRecommendationProviderData_scoreNetworksNotGranted()
+            throws Exception {
+        setNetworkRecommendationPackageNames("package1");
+        mockScoreNetworksDenied("package1");
+        mockRecommendationServiceAvailable("package1", 924 /* packageUid */);
+
+        assertNull(mNetworkScorerAppManager.getNetworkRecommendationProviderData());
+    }
+
+    public void testGetNetworkRecommendationProviderData_available() throws Exception {
+        setNetworkRecommendationPackageNames("package1");
+        mockScoreNetworksGranted("package1");
+        mockRecommendationServiceAvailable("package1", 924 /* packageUid */);
+
+        NetworkScorerAppData appData =
+                mNetworkScorerAppManager.getNetworkRecommendationProviderData();
+        assertNotNull(appData);
+        assertEquals("package1", appData.packageName);
+        assertEquals(924, appData.packageUid);
+        assertEquals(".RecommendationService", appData.recommendationServiceClassName);
+    }
+
+    public void testGetActiveScorer_providerAvailable() throws Exception {
+        setNetworkRecommendationPackageNames("package1");
+        mockScoreNetworksGranted("package1");
+        mockRecommendationServiceAvailable("package1", 924 /* packageUid */);
+
+        ContentResolver cr = mTargetContext.getContentResolver();
+        Settings.Global.putInt(cr, Settings.Global.NETWORK_RECOMMENDATIONS_ENABLED, 1);
+
+        final NetworkScorerAppData activeScorer = mNetworkScorerAppManager.getActiveScorer();
+        assertNotNull(activeScorer);
+        assertEquals("package1", activeScorer.packageName);
+        assertEquals(924, activeScorer.packageUid);
+        assertEquals(".RecommendationService", activeScorer.recommendationServiceClassName);
+    }
+
+    public void testGetActiveScorer_providerNotAvailable()
+            throws Exception {
+        ContentResolver cr = mTargetContext.getContentResolver();
+        Settings.Global.putInt(cr, Settings.Global.NETWORK_RECOMMENDATIONS_ENABLED, 1);
+
+        final NetworkScorerAppData activeScorer = mNetworkScorerAppManager.getActiveScorer();
+        assertNull(activeScorer);
+    }
+
+    public void testGetActiveScorer_recommendationsDisabled() throws Exception {
+        setNetworkRecommendationPackageNames("package1");
+        mockScoreNetworksGranted("package1");
+        mockRecommendationServiceAvailable("package1", 924 /* packageUid */);
+        ContentResolver cr = mTargetContext.getContentResolver();
+        Settings.Global.putInt(cr, Settings.Global.NETWORK_RECOMMENDATIONS_ENABLED, 0);
+
+        final NetworkScorerAppData activeScorer = mNetworkScorerAppManager.getActiveScorer();
+        assertNull(activeScorer);
+    }
+
+    private void setNetworkRecommendationPackageNames(String... names) {
+        if (names == null) {
+            names = new String[0];
         }
+        when(mResources.getStringArray(R.array.config_networkRecommendationPackageNames))
+                .thenReturn(names);
+    }
 
-        Mockito.when(mMockPm.queryBroadcastReceiversAsUser(
+    private void mockScoreNetworksGranted(String packageName) {
+        when(mMockPm.checkPermission(permission.SCORE_NETWORKS, packageName))
+                .thenReturn(PackageManager.PERMISSION_GRANTED);
+    }
+
+    private void mockScoreNetworksDenied(String packageName) {
+        when(mMockPm.checkPermission(permission.SCORE_NETWORKS, packageName))
+                .thenReturn(PackageManager.PERMISSION_DENIED);
+    }
+
+    private void mockRecommendationServiceAvailable(final String packageName, int packageUid) {
+        final ResolveInfo serviceInfo = new ResolveInfo();
+        serviceInfo.serviceInfo = new ServiceInfo();
+        serviceInfo.serviceInfo.name = ".RecommendationService";
+        serviceInfo.serviceInfo.packageName = packageName;
+        serviceInfo.serviceInfo.applicationInfo = new ApplicationInfo();
+        serviceInfo.serviceInfo.applicationInfo.uid = packageUid;
+
+        final int flags = 0;
+        when(mMockPm.resolveService(
                 Mockito.argThat(new ArgumentMatcher<Intent>() {
                     @Override
                     public boolean matches(Object object) {
                         Intent intent = (Intent) object;
-                        return NetworkScoreManager.ACTION_SCORE_NETWORKS.equals(intent.getAction());
+                        return NetworkScoreManager.ACTION_RECOMMEND_NETWORKS
+                                .equals(intent.getAction())
+                                && packageName.equals(intent.getPackage());
                     }
-                }), Mockito.eq(0), Mockito.eq(UserHandle.USER_SYSTEM)))
-                .thenReturn(receivers);
-    }
-
-    private ResolveInfoHolder buildResolveInfo(String packageName, int packageUid,
-            boolean hasReceiverPermission, boolean hasScorePermission, boolean hasConfigActivity,
-            boolean hasServiceInfo) throws Exception {
-        Mockito.when(mMockPm.checkPermission(permission.SCORE_NETWORKS, packageName))
-                .thenReturn(hasScorePermission ?
-                        PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED);
-
-        ResolveInfo resolveInfo = new ResolveInfo();
-        resolveInfo.activityInfo = new ActivityInfo();
-        resolveInfo.activityInfo.packageName = packageName;
-        resolveInfo.activityInfo.applicationInfo = new ApplicationInfo();
-        resolveInfo.activityInfo.applicationInfo.uid = packageUid;
-        if (hasReceiverPermission) {
-            resolveInfo.activityInfo.permission = permission.BROADCAST_NETWORK_PRIVILEGED;
-        }
-
-        ResolveInfo configActivityInfo = null;
-        if (hasConfigActivity) {
-            configActivityInfo = new ResolveInfo();
-            configActivityInfo.activityInfo = new ActivityInfo();
-            configActivityInfo.activityInfo.name = ".ConfigActivity";
-        }
-
-        ResolveInfo serviceInfo = null;
-        if (hasServiceInfo) {
-            serviceInfo = new ResolveInfo();
-            serviceInfo.serviceInfo = new ServiceInfo();
-            serviceInfo.serviceInfo.name = ".ScoringService";
-        }
-
-        return new ResolveInfoHolder(resolveInfo, configActivityInfo, serviceInfo);
-    }
-
-    private static class ResolveInfoHolder {
-        final ResolveInfo scorerResolveInfo;
-        final ResolveInfo configActivityResolveInfo;
-        final ResolveInfo serviceResolveInfo;
-
-        public ResolveInfoHolder(ResolveInfo scorerResolveInfo,
-                ResolveInfo configActivityResolveInfo, ResolveInfo serviceResolveInfo) {
-            this.scorerResolveInfo = scorerResolveInfo;
-            this.configActivityResolveInfo = configActivityResolveInfo;
-            this.serviceResolveInfo = serviceResolveInfo;
-        }
+                }), Mockito.eq(flags))).thenReturn(serviceInfo);
     }
 }
diff --git a/core/tests/coretests/src/android/net/RecommendationRequestTest.java b/core/tests/coretests/src/android/net/RecommendationRequestTest.java
new file mode 100644
index 0000000..31560b0
--- /dev/null
+++ b/core/tests/coretests/src/android/net/RecommendationRequestTest.java
@@ -0,0 +1,84 @@
+package android.net;
+
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+import android.os.Parcel;
+import android.test.AndroidTestCase;
+
+public class RecommendationRequestTest extends AndroidTestCase {
+    private ScanResult[] mScanResults;
+    private WifiConfiguration mConfiguration;
+    private NetworkCapabilities mCapabilities;
+
+    @Override
+    public void setUp() throws Exception {
+        mScanResults = new ScanResult[2];
+        mScanResults[0] = new ScanResult();
+        mScanResults[1] = new ScanResult(
+                "ssid",
+                "bssid",
+                0L /*hessid*/,
+                1 /*anqpDominId*/,
+                "caps",
+                2 /*level*/,
+                3 /*frequency*/,
+                4L /*tsf*/,
+                5 /*distCm*/,
+                6 /*distSdCm*/,
+                7 /*channelWidth*/,
+                8 /*centerFreq0*/,
+                9 /*centerFreq1*/,
+                false /*is80211McRTTResponder*/);
+        mConfiguration = new WifiConfiguration();
+        mConfiguration.SSID = "RecommendationRequestTest";
+        mCapabilities = new NetworkCapabilities()
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED);
+    }
+
+    public void testParceling() throws Exception {
+        RecommendationRequest request = new RecommendationRequest.Builder()
+                .setCurrentRecommendedWifiConfig(mConfiguration)
+                .setScanResults(mScanResults)
+                .setNetworkCapabilities(mCapabilities)
+                .build();
+
+        RecommendationRequest parceled = passThroughParcel(request);
+        assertEquals(request.getCurrentSelectedConfig().SSID,
+                parceled.getCurrentSelectedConfig().SSID);
+        assertEquals(request.getRequiredCapabilities(), parceled.getRequiredCapabilities());
+        ScanResult[] parceledScanResults = parceled.getScanResults();
+        assertNotNull(parceledScanResults);
+        assertEquals(mScanResults.length, parceledScanResults.length);
+        for (int i = 0; i < mScanResults.length; i++) {
+            assertEquals(mScanResults[i].SSID, parceledScanResults[i].SSID);
+        }
+    }
+
+    public void testParceling_nullScanResults() throws Exception {
+        RecommendationRequest request = new RecommendationRequest.Builder()
+                .setCurrentRecommendedWifiConfig(mConfiguration)
+                .setNetworkCapabilities(mCapabilities)
+                .build();
+
+        RecommendationRequest parceled = passThroughParcel(request);
+        assertEquals(request.getCurrentSelectedConfig().SSID,
+                parceled.getCurrentSelectedConfig().SSID);
+        assertEquals(request.getRequiredCapabilities(), parceled.getRequiredCapabilities());
+        ScanResult[] parceledScanResults = parceled.getScanResults();
+        assertNull(parceledScanResults);
+    }
+
+    private RecommendationRequest passThroughParcel(RecommendationRequest request) {
+        Parcel p = Parcel.obtain();
+        RecommendationRequest output = null;
+        try {
+            request.writeToParcel(p, 0);
+            p.setDataPosition(0);
+            output = RecommendationRequest.CREATOR.createFromParcel(p);
+        } finally {
+            p.recycle();
+        }
+        assertNotNull(output);
+        return output;
+    }
+}
diff --git a/services/core/java/com/android/server/NetworkScoreService.java b/services/core/java/com/android/server/NetworkScoreService.java
index a1c3564..f712f12 100644
--- a/services/core/java/com/android/server/NetworkScoreService.java
+++ b/services/core/java/com/android/server/NetworkScoreService.java
@@ -16,7 +16,11 @@
 
 package com.android.server;
 
+import static android.net.NetworkRecommendationProvider.EXTRA_RECOMMENDATION_RESULT;
+import static android.net.NetworkRecommendationProvider.EXTRA_SEQUENCE;
+
 import android.Manifest.permission;
+import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentResolver;
@@ -25,27 +29,29 @@
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
 import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.net.INetworkRecommendationProvider;
 import android.net.INetworkScoreCache;
 import android.net.INetworkScoreService;
 import android.net.NetworkKey;
-import android.net.NetworkScoreManager;
 import android.net.NetworkScorerAppManager;
 import android.net.NetworkScorerAppManager.NetworkScorerAppData;
 import android.net.RecommendationRequest;
 import android.net.RecommendationResult;
 import android.net.ScoredNetwork;
+import android.net.Uri;
 import android.net.wifi.WifiConfiguration;
-import android.os.Binder;
+import android.os.Bundle;
 import android.os.IBinder;
+import android.os.IRemoteCallback;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.UserHandle;
-import android.provider.Settings;
-import android.text.TextUtils;
+import android.provider.Settings.Global;
 import android.util.ArrayMap;
 import android.util.Log;
+import android.util.TimedRemoteCaller;
 
-import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.PackageMonitor;
@@ -59,6 +65,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.TimeoutException;
 import java.util.function.Consumer;
 
 /**
@@ -67,17 +74,20 @@
  */
 public class NetworkScoreService extends INetworkScoreService.Stub {
     private static final String TAG = "NetworkScoreService";
-    private static final boolean DBG = false;
+    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
 
     private final Context mContext;
     private final NetworkScorerAppManager mNetworkScorerAppManager;
+    private final RequestRecommendationCaller mRequestRecommendationCaller;
     @GuardedBy("mScoreCaches")
     private final Map<Integer, RemoteCallbackList<INetworkScoreCache>> mScoreCaches;
     /** Lock used to update mPackageMonitor when scorer package changes occur. */
     private final Object mPackageMonitorLock = new Object[0];
+    private final Object mServiceConnectionLock = new Object[0];
 
     @GuardedBy("mPackageMonitorLock")
     private NetworkScorerPackageMonitor mPackageMonitor;
+    @GuardedBy("mServiceConnectionLock")
     private ScoringServiceConnection mServiceConnection;
 
     private BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() {
@@ -99,10 +109,10 @@
      * manages the service connection.
      */
     private class NetworkScorerPackageMonitor extends PackageMonitor {
-        final String mRegisteredPackage;
+        final List<String> mPackagesToWatch;
 
-        private NetworkScorerPackageMonitor(String mRegisteredPackage) {
-            this.mRegisteredPackage = mRegisteredPackage;
+        private NetworkScorerPackageMonitor(List<String> packagesToWatch) {
+            mPackagesToWatch = packagesToWatch;
         }
 
         @Override
@@ -136,7 +146,7 @@
         }
 
         private void evaluateBinding(String scorerPackageName, boolean forceUnbind) {
-            if (mRegisteredPackage.equals(scorerPackageName)) {
+            if (mPackagesToWatch.contains(scorerPackageName)) {
                 if (DBG) {
                     Log.d(TAG, "Evaluating binding for: " + scorerPackageName
                             + ", forceUnbind=" + forceUnbind);
@@ -146,13 +156,14 @@
                 if (activeScorer == null) {
                     // Package change has invalidated a scorer, this will also unbind any service
                     // connection.
-                    Log.i(TAG, "Package " + mRegisteredPackage +
-                            " is no longer valid, disabling scoring.");
-                    setScorerInternal(null);
-                } else if (activeScorer.mScoringServiceClassName == null) {
-                    // The scoring service is not available, make sure it's unbound.
+                    if (DBG) Log.d(TAG, "No active scorers available.");
                     unbindFromScoringServiceIfNeeded();
-                } else { // The scoring service changed in some way.
+                } else if (activeScorer.packageName.equals(scorerPackageName)) {
+                    if (DBG) {
+                        Log.d(TAG, "Possible change to the active scorer: "
+                            + activeScorer.packageName);
+                    }
+                    // The scoring service changed in some way.
                     if (forceUnbind) {
                         unbindFromScoringServiceIfNeeded();
                     }
@@ -162,6 +173,27 @@
         }
     }
 
+    /**
+     * Reevaluates the service binding when the Settings toggle is changed.
+     */
+    private class SettingsObserver extends ContentObserver {
+
+        public SettingsObserver() {
+            super(null /*handler*/);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            onChange(selfChange, null);
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            if (DBG) Log.d(TAG, String.format("onChange(%s, %s)", selfChange, uri));
+            bindToScoringServiceIfNeeded();
+        }
+    }
+
     public NetworkScoreService(Context context) {
       this(context, new NetworkScorerAppManager(context));
     }
@@ -176,24 +208,16 @@
         mContext.registerReceiverAsUser(
                 mUserIntentReceiver, UserHandle.SYSTEM, filter, null /* broadcastPermission*/,
                 null /* scheduler */);
+        // TODO(jjoslin): 12/15/16 - Make timeout configurable.
+        mRequestRecommendationCaller =
+            new RequestRecommendationCaller(TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS);
     }
 
     /** Called when the system is ready to run third-party code but before it actually does so. */
     void systemReady() {
         if (DBG) Log.d(TAG, "systemReady");
-        ContentResolver cr = mContext.getContentResolver();
-        if (Settings.Global.getInt(cr, Settings.Global.NETWORK_SCORING_PROVISIONED, 0) == 0) {
-            // On first run, we try to initialize the scorer to the one configured at build time.
-            // This will be a no-op if the scorer isn't actually valid.
-            String defaultPackage = mContext.getResources().getString(
-                    R.string.config_defaultNetworkScorerPackageName);
-            if (!TextUtils.isEmpty(defaultPackage)) {
-                mNetworkScorerAppManager.setActiveScorer(defaultPackage);
-            }
-            Settings.Global.putInt(cr, Settings.Global.NETWORK_SCORING_PROVISIONED, 1);
-        }
-
         registerPackageMonitorIfNeeded();
+        registerRecommendationSettingObserverIfNeeded();
     }
 
     /** Called when the system is ready for us to start third-party code. */
@@ -207,29 +231,40 @@
         bindToScoringServiceIfNeeded();
     }
 
+    private void registerRecommendationSettingObserverIfNeeded() {
+        final List<String> providerPackages =
+            mNetworkScorerAppManager.getPotentialRecommendationProviderPackages();
+        if (!providerPackages.isEmpty()) {
+            final ContentResolver resolver = mContext.getContentResolver();
+            final Uri uri = Global.getUriFor(Global.NETWORK_RECOMMENDATIONS_ENABLED);
+            resolver.registerContentObserver(uri, false, new SettingsObserver());
+        }
+    }
+
     private void registerPackageMonitorIfNeeded() {
         if (DBG) Log.d(TAG, "registerPackageMonitorIfNeeded");
-        NetworkScorerAppData scorer = mNetworkScorerAppManager.getActiveScorer();
+        final List<String> providerPackages =
+            mNetworkScorerAppManager.getPotentialRecommendationProviderPackages();
         synchronized (mPackageMonitorLock) {
             // Unregister the current monitor if needed.
             if (mPackageMonitor != null) {
                 if (DBG) {
                     Log.d(TAG, "Unregistering package monitor for "
-                            + mPackageMonitor.mRegisteredPackage);
+                            + mPackageMonitor.mPackagesToWatch);
                 }
                 mPackageMonitor.unregister();
                 mPackageMonitor = null;
             }
 
-            // Create and register the monitor if a scorer is active.
-            if (scorer != null) {
-                mPackageMonitor = new NetworkScorerPackageMonitor(scorer.mPackageName);
+            // Create and register the monitor if there are packages that could be providers.
+            if (!providerPackages.isEmpty()) {
+                mPackageMonitor = new NetworkScorerPackageMonitor(providerPackages);
                 // TODO: Need to update when we support per-user scorers. http://b/23422763
                 mPackageMonitor.register(mContext, null /* thread */, UserHandle.SYSTEM,
                         false /* externalStorage */);
                 if (DBG) {
                     Log.d(TAG, "Registered package monitor for "
-                            + mPackageMonitor.mRegisteredPackage);
+                            + mPackageMonitor.mPackagesToWatch);
                 }
             }
         }
@@ -243,22 +278,24 @@
 
     private void bindToScoringServiceIfNeeded(NetworkScorerAppData scorerData) {
         if (DBG) Log.d(TAG, "bindToScoringServiceIfNeeded(" + scorerData + ")");
-        if (scorerData != null && scorerData.mScoringServiceClassName != null) {
-            ComponentName componentName =
-                    new ComponentName(scorerData.mPackageName, scorerData.mScoringServiceClassName);
-            // If we're connected to a different component then drop it.
-            if (mServiceConnection != null
-                    && !mServiceConnection.mComponentName.equals(componentName)) {
-                unbindFromScoringServiceIfNeeded();
-            }
+        if (scorerData != null && scorerData.recommendationServiceClassName != null) {
+            ComponentName componentName = new ComponentName(scorerData.packageName,
+                    scorerData.recommendationServiceClassName);
+            synchronized (mServiceConnectionLock) {
+                // If we're connected to a different component then drop it.
+                if (mServiceConnection != null
+                        && !mServiceConnection.mComponentName.equals(componentName)) {
+                    unbindFromScoringServiceIfNeeded();
+                }
 
-            // If we're not connected at all then create a new connection.
-            if (mServiceConnection == null) {
-                mServiceConnection = new ScoringServiceConnection(componentName);
-            }
+                // If we're not connected at all then create a new connection.
+                if (mServiceConnection == null) {
+                    mServiceConnection = new ScoringServiceConnection(componentName);
+                }
 
-            // Make sure the connection is connected (idempotent)
-            mServiceConnection.connect(mContext);
+                // Make sure the connection is connected (idempotent)
+                mServiceConnection.connect(mContext);
+            }
         } else { // otherwise make sure it isn't bound.
             unbindFromScoringServiceIfNeeded();
         }
@@ -266,10 +303,13 @@
 
     private void unbindFromScoringServiceIfNeeded() {
         if (DBG) Log.d(TAG, "unbindFromScoringServiceIfNeeded");
-        if (mServiceConnection != null) {
-            mServiceConnection.disconnect(mContext);
+        synchronized (mServiceConnectionLock) {
+            if (mServiceConnection != null) {
+                mServiceConnection.disconnect(mContext);
+            }
+            mServiceConnection = null;
         }
-        mServiceConnection = null;
+        clearInternal();
     }
 
     @Override
@@ -349,7 +389,8 @@
         // mContext.enforceCallingOrSelfPermission(permission.BROADCAST_NETWORK_PRIVILEGED, TAG);
         mContext.enforceCallingOrSelfPermission(permission.SCORE_NETWORKS, TAG);
 
-        return setScorerInternal(packageName);
+        // Scorers (recommendation providers) are selected and no longer set.
+        return false;
     }
 
     @Override
@@ -359,56 +400,13 @@
         if (mNetworkScorerAppManager.isCallerActiveScorer(getCallingUid()) ||
                 mContext.checkCallingOrSelfPermission(permission.BROADCAST_NETWORK_PRIVILEGED) ==
                         PackageManager.PERMISSION_GRANTED) {
-            // The return value is discarded here because at this point, the call should always
-            // succeed. The only reason for failure is if the new package is not a valid scorer, but
-            // we're disabling scoring altogether here.
-            setScorerInternal(null /* packageName */);
+            // no-op for now but we could write to the setting if needed.
         } else {
             throw new SecurityException(
                     "Caller is neither the active scorer nor the scorer manager.");
         }
     }
 
-    /** Set the active scorer. Callers are responsible for checking permissions as appropriate. */
-    private boolean setScorerInternal(String packageName) {
-        if (DBG) Log.d(TAG, "setScorerInternal(" + packageName + ")");
-        long token = Binder.clearCallingIdentity();
-        try {
-            unbindFromScoringServiceIfNeeded();
-            // Preemptively clear scores even though the set operation could fail. We do this for
-            // safety as scores should never be compared across apps; in practice, Settings should
-            // only be allowing valid apps to be set as scorers, so failure here should be rare.
-            clearInternal();
-            // Get the scorer that is about to be replaced, if any, so we can notify it directly.
-            NetworkScorerAppData prevScorer = mNetworkScorerAppManager.getActiveScorer();
-            boolean result = mNetworkScorerAppManager.setActiveScorer(packageName);
-            // Unconditionally attempt to bind to the current scorer. If setActiveScorer() failed
-            // then we'll attempt to restore the previous binding (if any), otherwise an attempt
-            // will be made to bind to the new scorer.
-            bindToScoringServiceIfNeeded();
-            if (result) { // new scorer successfully set
-                registerPackageMonitorIfNeeded();
-
-                Intent intent = new Intent(NetworkScoreManager.ACTION_SCORER_CHANGED);
-                if (prevScorer != null) { // Directly notify the old scorer.
-                    intent.setPackage(prevScorer.mPackageName);
-                    // TODO: Need to update when we support per-user scorers. http://b/23422763
-                    mContext.sendBroadcastAsUser(intent, UserHandle.SYSTEM);
-                }
-
-                if (packageName != null) { // Then notify the new scorer
-                    intent.putExtra(NetworkScoreManager.EXTRA_NEW_SCORER, packageName);
-                    intent.setPackage(packageName);
-                    // TODO: Need to update when we support per-user scorers. http://b/23422763
-                    mContext.sendBroadcastAsUser(intent, UserHandle.SYSTEM);
-                }
-            }
-            return result;
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
     /** Clear scores. Callers are responsible for checking permissions as appropriate. */
     private void clearInternal() {
         sendCallback(new Consumer<INetworkScoreCache>() {
@@ -464,7 +462,22 @@
 
     @Override
     public RecommendationResult requestRecommendation(RecommendationRequest request) {
-        // TODO(jjoslin): 11/25/16 - Update with real impl.
+        mContext.enforceCallingOrSelfPermission(permission.BROADCAST_NETWORK_PRIVILEGED, TAG);
+        throwIfCalledOnMainThread();
+        final INetworkRecommendationProvider provider = getRecommendationProvider();
+        if (provider != null) {
+            try {
+                return mRequestRecommendationCaller.getRecommendationResult(provider, request);
+            } catch (RemoteException | TimeoutException e) {
+                Log.w(TAG, "Failed to request a recommendation.", e);
+                // TODO(jjoslin): 12/15/16 - Keep track of failures.
+            }
+        }
+
+        if (DBG) {
+            Log.d(TAG, "Returning the default network recommendation.");
+        }
+
         WifiConfiguration selectedConfig = null;
         if (request != null) {
             selectedConfig = request.getCurrentSelectedConfig();
@@ -474,7 +487,19 @@
 
     @Override
     public boolean requestScores(NetworkKey[] networks) {
-        // TODO(jjoslin): 12/13/16 - Implement
+        mContext.enforceCallingOrSelfPermission(permission.BROADCAST_NETWORK_PRIVILEGED, TAG);
+        final INetworkRecommendationProvider provider = getRecommendationProvider();
+        if (provider != null) {
+            try {
+                provider.requestScores(networks);
+                // TODO(jjoslin): 12/15/16 - Consider pushing null scores into the cache to prevent
+                // repeated requests for the same scores.
+                return true;
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed to request scores.", e);
+                // TODO(jjoslin): 12/15/16 - Keep track of failures.
+            }
+        }
         return false;
     }
 
@@ -486,7 +511,7 @@
             writer.println("Scoring is disabled.");
             return;
         }
-        writer.println("Current scorer: " + currentScorer.mPackageName);
+        writer.println("Current scorer: " + currentScorer.packageName);
 
         sendCallback(new Consumer<INetworkScoreCache>() {
             @Override
@@ -499,10 +524,12 @@
             }
         }, getScoreCacheLists());
 
-        if (mServiceConnection != null) {
-            mServiceConnection.dump(fd, writer, args);
-        } else {
-            writer.println("ScoringServiceConnection: null");
+        synchronized (mServiceConnectionLock) {
+            if (mServiceConnection != null) {
+                mServiceConnection.dump(fd, writer, args);
+            } else {
+                writer.println("ScoringServiceConnection: null");
+            }
         }
         writer.flush();
     }
@@ -535,10 +562,27 @@
         }
     }
 
+    private void throwIfCalledOnMainThread() {
+        if (Thread.currentThread() == mContext.getMainLooper().getThread()) {
+            throw new RuntimeException("Cannot invoke on the main thread");
+        }
+    }
+
+    @Nullable
+    private INetworkRecommendationProvider getRecommendationProvider() {
+        synchronized (mServiceConnectionLock) {
+            if (mServiceConnection != null) {
+                return mServiceConnection.getRecommendationProvider();
+            }
+        }
+        return null;
+    }
+
     private static class ScoringServiceConnection implements ServiceConnection {
         private final ComponentName mComponentName;
-        private boolean mBound = false;
-        private boolean mConnected = false;
+        private volatile boolean mBound = false;
+        private volatile boolean mConnected = false;
+        private volatile INetworkRecommendationProvider mRecommendationProvider;
 
         ScoringServiceConnection(ComponentName componentName) {
             mComponentName = componentName;
@@ -569,12 +613,19 @@
             } catch (RuntimeException e) {
                 Log.e(TAG, "Unbind failed.", e);
             }
+
+            mRecommendationProvider = null;
+        }
+
+        INetworkRecommendationProvider getRecommendationProvider() {
+            return mRecommendationProvider;
         }
 
         @Override
         public void onServiceConnected(ComponentName name, IBinder service) {
             if (DBG) Log.d(TAG, "ScoringServiceConnection: " + name.flattenToString());
             mConnected = true;
+            mRecommendationProvider = INetworkRecommendationProvider.Stub.asInterface(service);
         }
 
         @Override
@@ -583,6 +634,7 @@
                 Log.d(TAG, "ScoringServiceConnection, disconnected: " + name.flattenToString());
             }
             mConnected = false;
+            mRecommendationProvider = null;
         }
 
         public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
@@ -590,4 +642,43 @@
                     + ", connected: " + mConnected);
         }
     }
+
+    /**
+     * Executes the async requestRecommendation() call with a timeout.
+     */
+    private static final class RequestRecommendationCaller
+            extends TimedRemoteCaller<RecommendationResult> {
+        private final IRemoteCallback mCallback;
+
+        RequestRecommendationCaller(long callTimeoutMillis) {
+            super(callTimeoutMillis);
+            mCallback = new IRemoteCallback.Stub() {
+                @Override
+                public void sendResult(Bundle data) throws RemoteException {
+                    final RecommendationResult result =
+                            data.getParcelable(EXTRA_RECOMMENDATION_RESULT);
+                    final int sequence = data.getInt(EXTRA_SEQUENCE, -1);
+                    onRemoteMethodResult(result, sequence);
+                }
+            };
+        }
+
+        /**
+         * Runs the requestRecommendation() call on the given {@link INetworkRecommendationProvider}
+         * instance.
+         *
+         * @param target the {@link INetworkRecommendationProvider} to request a recommendation
+         *               from
+         * @param request the {@link RecommendationRequest} from the calling client
+         * @return a {@link RecommendationResult} from the provider
+         * @throws RemoteException if the call failed
+         * @throws TimeoutException if the call took longer than the set timeout
+         */
+        RecommendationResult getRecommendationResult(INetworkRecommendationProvider target,
+                RecommendationRequest request) throws RemoteException, TimeoutException {
+            final int sequence = onBeforeRemoteCall();
+            target.requestRecommendation(request, mCallback, sequence);
+            return getResultTimed(sequence);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/pm/Installer.java b/services/core/java/com/android/server/pm/Installer.java
index 3193974..203f841 100644
--- a/services/core/java/com/android/server/pm/Installer.java
+++ b/services/core/java/com/android/server/pm/Installer.java
@@ -105,11 +105,11 @@
         }
     }
 
-    public void createAppData(String uuid, String packageName, int userId, int flags, int appId,
+    public long createAppData(String uuid, String packageName, int userId, int flags, int appId,
             String seInfo, int targetSdkVersion) throws InstallerException {
-        if (!checkBeforeRemote()) return;
+        if (!checkBeforeRemote()) return -1;
         try {
-            mInstalld.createAppData(uuid, packageName, userId, flags, appId, seInfo,
+            return mInstalld.createAppData(uuid, packageName, userId, flags, appId, seInfo,
                     targetSdkVersion);
         } catch (Exception e) {
             throw InstallerException.from(e);
@@ -182,16 +182,6 @@
         }
     }
 
-    public long getAppDataInode(String uuid, String packageName, int userId, int flags)
-            throws InstallerException {
-        if (!checkBeforeRemote()) return -1;
-        try {
-            return mInstalld.getAppDataInode(uuid, packageName, userId, flags);
-        } catch (Exception e) {
-            throw InstallerException.from(e);
-        }
-    }
-
     public void dexopt(String apkPath, int uid, @Nullable String pkgName, String instructionSet,
             int dexoptNeeded, @Nullable String outputPath, int dexFlags,
             String compilerFilter, @Nullable String volumeUuid, @Nullable String sharedLibraries)
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index fb06f01..1c78b16 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -19969,8 +19969,9 @@
 
         Preconditions.checkNotNull(app.seinfo);
 
+        long ceDataInode = -1;
         try {
-            mInstaller.createAppData(volumeUuid, packageName, userId, flags,
+            ceDataInode = mInstaller.createAppData(volumeUuid, packageName, userId, flags,
                     appId, app.seinfo, app.targetSdkVersion);
         } catch (InstallerException e) {
             if (app.isSystemApp()) {
@@ -19978,7 +19979,7 @@
                         + ", but trying to recover: " + e);
                 destroyAppDataLeafLIF(pkg, userId, flags);
                 try {
-                    mInstaller.createAppData(volumeUuid, packageName, userId, flags,
+                    ceDataInode = mInstaller.createAppData(volumeUuid, packageName, userId, flags,
                             appId, app.seinfo, app.targetSdkVersion);
                     logCriticalInfo(Log.DEBUG, "Recovery succeeded!");
                 } catch (InstallerException e2) {
@@ -19989,21 +19990,13 @@
             }
         }
 
-        if ((flags & StorageManager.FLAG_STORAGE_CE) != 0) {
-            try {
-                // CE storage is unlocked right now, so read out the inode and
-                // remember for use later when it's locked
-                // TODO: mark this structure as dirty so we persist it!
-                final long ceDataInode = mInstaller.getAppDataInode(volumeUuid, packageName, userId,
-                        StorageManager.FLAG_STORAGE_CE);
-                synchronized (mPackages) {
-                    final PackageSetting ps = mSettings.mPackages.get(packageName);
-                    if (ps != null) {
-                        ps.setCeDataInode(ceDataInode, userId);
-                    }
+        if ((flags & StorageManager.FLAG_STORAGE_CE) != 0 && ceDataInode != -1) {
+            // TODO: mark this structure as dirty so we persist it!
+            synchronized (mPackages) {
+                final PackageSetting ps = mSettings.mPackages.get(packageName);
+                if (ps != null) {
+                    ps.setCeDataInode(ceDataInode, userId);
                 }
-            } catch (InstallerException e) {
-                Slog.e(TAG, "Failed to find inode for " + packageName + ": " + e);
             }
         }
 
diff --git a/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java
index 50911cb..c653b8e 100644
--- a/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java
@@ -16,10 +16,13 @@
 
 package com.android.server;
 
+import static android.net.NetworkRecommendationProvider.EXTRA_RECOMMENDATION_RESULT;
+import static android.net.NetworkRecommendationProvider.EXTRA_SEQUENCE;
 import static android.net.NetworkScoreManager.CACHE_FILTER_NONE;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.assertTrue;
 import static junit.framework.Assert.fail;
 
@@ -28,12 +31,15 @@
 import static org.mockito.Matchers.anyListOf;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isA;
 import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
 import android.Manifest.permission;
@@ -44,37 +50,42 @@
 import android.content.ServiceConnection;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
+import android.net.INetworkRecommendationProvider;
 import android.net.INetworkScoreCache;
 import android.net.NetworkKey;
-import android.net.NetworkScoreManager;
 import android.net.NetworkScorerAppManager;
 import android.net.NetworkScorerAppManager.NetworkScorerAppData;
+import android.net.RecommendationRequest;
+import android.net.RecommendationResult;
 import android.net.ScoredNetwork;
 import android.net.WifiKey;
+import android.net.wifi.WifiConfiguration;
+import android.os.Bundle;
 import android.os.IBinder;
+import android.os.IRemoteCallback;
+import android.os.Looper;
 import android.os.RemoteException;
 import android.os.UserHandle;
-import android.provider.Settings;
-import android.provider.Settings.Global;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.MediumTest;
 import android.support.test.runner.AndroidJUnit4;
 
-import com.android.internal.R;
 import com.android.server.devicepolicy.MockUtils;
 
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.util.List;
 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.Mockito;
 import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.List;
 
 /**
  * Tests for {@link NetworkScoreService}.
@@ -85,12 +96,8 @@
     private static final ScoredNetwork SCORED_NETWORK =
             new ScoredNetwork(new NetworkKey(new WifiKey("\"ssid\"", "00:00:00:00:00:00")),
                     null /* rssiCurve*/);
-    private static final NetworkScorerAppData PREV_SCORER = new NetworkScorerAppData(
-            "prevPackageName", 0, "prevScorerName", null /* configurationActivityClassName */,
-            "prevScoringServiceClass");
-    private static final NetworkScorerAppData NEW_SCORER = new NetworkScorerAppData(
-            "newPackageName", 1, "newScorerName", null /* configurationActivityClassName */,
-            "newScoringServiceClass");
+    private static final NetworkScorerAppData NEW_SCORER =
+        new NetworkScorerAppData("newPackageName", 1, "newScoringServiceClass");
 
     @Mock private PackageManager mPackageManager;
     @Mock private NetworkScorerAppManager mNetworkScorerAppManager;
@@ -98,10 +105,12 @@
     @Mock private Resources mResources;
     @Mock private INetworkScoreCache.Stub mNetworkScoreCache, mNetworkScoreCache2;
     @Mock private IBinder mIBinder, mIBinder2;
+    @Mock private INetworkRecommendationProvider mRecommendationProvider;
     @Captor private ArgumentCaptor<List<ScoredNetwork>> mScoredNetworkCaptor;
 
     private ContentResolver mContentResolver;
     private NetworkScoreService mNetworkScoreService;
+    private RecommendationRequest mRecommendationRequest;
 
     @Before
     public void setUp() throws Exception {
@@ -112,44 +121,9 @@
         when(mContext.getContentResolver()).thenReturn(mContentResolver);
         when(mContext.getResources()).thenReturn(mResources);
         mNetworkScoreService = new NetworkScoreService(mContext, mNetworkScorerAppManager);
-    }
-
-    @Test
-    public void testSystemReady_networkScorerProvisioned() throws Exception {
-        Settings.Global.putInt(mContentResolver, Global.NETWORK_SCORING_PROVISIONED, 1);
-
-        mNetworkScoreService.systemReady();
-
-        verify(mNetworkScorerAppManager, never()).setActiveScorer(anyString());
-    }
-
-    @Test
-    public void testSystemReady_networkScorerNotProvisioned_defaultScorer() throws Exception {
-        Settings.Global.putInt(mContentResolver, Global.NETWORK_SCORING_PROVISIONED, 0);
-
-        when(mResources.getString(R.string.config_defaultNetworkScorerPackageName))
-                .thenReturn(NEW_SCORER.mPackageName);
-
-        mNetworkScoreService.systemReady();
-
-        verify(mNetworkScorerAppManager).setActiveScorer(NEW_SCORER.mPackageName);
-        assertEquals(1,
-                Settings.Global.getInt(mContentResolver, Global.NETWORK_SCORING_PROVISIONED));
-
-    }
-
-    @Test
-    public void testSystemReady_networkScorerNotProvisioned_noDefaultScorer() throws Exception {
-        Settings.Global.putInt(mContentResolver, Global.NETWORK_SCORING_PROVISIONED, 0);
-
-        when(mResources.getString(R.string.config_defaultNetworkScorerPackageName))
-                .thenReturn(null);
-
-        mNetworkScoreService.systemReady();
-
-        verify(mNetworkScorerAppManager, never()).setActiveScorer(anyString());
-        assertEquals(1,
-                Settings.Global.getInt(mContentResolver, Global.NETWORK_SCORING_PROVISIONED));
+        WifiConfiguration configuration = new WifiConfiguration();
+        mRecommendationRequest = new RecommendationRequest.Builder()
+            .setCurrentRecommendedWifiConfig(configuration).build();
     }
 
     @Test
@@ -159,13 +133,126 @@
         mNetworkScoreService.systemRunning();
 
         verify(mContext).bindServiceAsUser(MockUtils.checkIntent(new Intent().setComponent(
-                new ComponentName(NEW_SCORER.mPackageName, NEW_SCORER.mScoringServiceClassName))),
+                new ComponentName(NEW_SCORER.packageName,
+                    NEW_SCORER.recommendationServiceClassName))),
                 any(ServiceConnection.class),
                 eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE),
                 eq(UserHandle.SYSTEM));
     }
 
     @Test
+    public void testRequestScores_noPermission() throws Exception {
+        doThrow(new SecurityException()).when(mContext)
+            .enforceCallingOrSelfPermission(eq(permission.BROADCAST_NETWORK_PRIVILEGED),
+                anyString());
+        try {
+            mNetworkScoreService.requestScores(null);
+            fail("BROADCAST_NETWORK_PRIVILEGED not enforced.");
+        } catch (SecurityException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testRequestScores_providerNotConnected() throws Exception {
+        assertFalse(mNetworkScoreService.requestScores(new NetworkKey[0]));
+        verifyZeroInteractions(mRecommendationProvider);
+    }
+
+    @Test
+    public void testRequestScores_providerThrowsRemoteException() throws Exception {
+        injectProvider();
+        doThrow(new RemoteException()).when(mRecommendationProvider)
+            .requestScores(any(NetworkKey[].class));
+
+        assertFalse(mNetworkScoreService.requestScores(new NetworkKey[0]));
+    }
+
+    @Test
+    public void testRequestScores_providerAvailable() throws Exception {
+        injectProvider();
+
+        final NetworkKey[] networks = new NetworkKey[0];
+        assertTrue(mNetworkScoreService.requestScores(networks));
+        verify(mRecommendationProvider).requestScores(networks);
+    }
+
+    @Test
+    public void testRequestRecommendation_noPermission() throws Exception {
+        doThrow(new SecurityException()).when(mContext)
+            .enforceCallingOrSelfPermission(eq(permission.BROADCAST_NETWORK_PRIVILEGED),
+                anyString());
+        try {
+            mNetworkScoreService.requestRecommendation(mRecommendationRequest);
+            fail("BROADCAST_NETWORK_PRIVILEGED not enforced.");
+        } catch (SecurityException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testRequestRecommendation_mainThread() throws Exception {
+        when(mContext.getMainLooper()).thenReturn(Looper.myLooper());
+        try {
+            mNetworkScoreService.requestRecommendation(mRecommendationRequest);
+            fail("requestRecommendation run on main thread.");
+        } catch (RuntimeException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testRequestRecommendation_providerNotConnected() throws Exception {
+        when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper());
+
+        final RecommendationResult result =
+                mNetworkScoreService.requestRecommendation(mRecommendationRequest);
+        assertNotNull(result);
+        assertEquals(mRecommendationRequest.getCurrentSelectedConfig(),
+                result.getWifiConfiguration());
+    }
+
+    @Test
+    public void testRequestRecommendation_providerThrowsRemoteException() throws Exception {
+        injectProvider();
+        when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper());
+        doThrow(new RemoteException()).when(mRecommendationProvider)
+                .requestRecommendation(eq(mRecommendationRequest), isA(IRemoteCallback.class),
+                        anyInt());
+
+        final RecommendationResult result =
+                mNetworkScoreService.requestRecommendation(mRecommendationRequest);
+        assertNotNull(result);
+        assertEquals(mRecommendationRequest.getCurrentSelectedConfig(),
+                result.getWifiConfiguration());
+    }
+
+    @Test
+    public void testRequestRecommendation_resultReturned() throws Exception {
+        injectProvider();
+        when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper());
+        final WifiConfiguration wifiConfiguration = new WifiConfiguration();
+        wifiConfiguration.SSID = "testRequestRecommendation_resultReturned";
+        final RecommendationResult providerResult =
+                new RecommendationResult(wifiConfiguration);
+        final Bundle bundle = new Bundle();
+        bundle.putParcelable(EXTRA_RECOMMENDATION_RESULT, providerResult);
+        doAnswer(invocation -> {
+            bundle.putInt(EXTRA_SEQUENCE, invocation.getArgumentAt(2, int.class));
+            invocation.getArgumentAt(1, IRemoteCallback.class).sendResult(bundle);
+            return null;
+        }).when(mRecommendationProvider)
+                .requestRecommendation(eq(mRecommendationRequest), isA(IRemoteCallback.class),
+                        anyInt());
+
+        final RecommendationResult result =
+                mNetworkScoreService.requestRecommendation(mRecommendationRequest);
+        assertNotNull(result);
+        assertEquals(providerResult.getWifiConfiguration().SSID,
+                result.getWifiConfiguration().SSID);
+    }
+
+    @Test
     public void testUpdateScores_notActiveScorer() {
         when(mNetworkScorerAppManager.isCallerActiveScorer(anyInt())).thenReturn(false);
 
@@ -288,45 +375,6 @@
     }
 
     @Test
-    public void testSetActiveScorer_failure() throws RemoteException {
-        when(mNetworkScorerAppManager.getActiveScorer()).thenReturn(PREV_SCORER);
-        when(mNetworkScorerAppManager.setActiveScorer(NEW_SCORER.mPackageName)).thenReturn(false);
-        mNetworkScoreService.registerNetworkScoreCache(NetworkKey.TYPE_WIFI, mNetworkScoreCache,
-                CACHE_FILTER_NONE);
-
-        boolean success = mNetworkScoreService.setActiveScorer(NEW_SCORER.mPackageName);
-
-        assertFalse(success);
-        verify(mNetworkScoreCache).clearScores();
-        verify(mContext).bindServiceAsUser(MockUtils.checkIntent(new Intent().setComponent(
-                new ComponentName(PREV_SCORER.mPackageName, PREV_SCORER.mScoringServiceClassName))),
-                any(ServiceConnection.class),
-                eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE),
-                eq(UserHandle.SYSTEM));
-    }
-
-    @Test
-    public void testSetActiveScorer_success() throws RemoteException {
-        when(mNetworkScorerAppManager.getActiveScorer()).thenReturn(PREV_SCORER, NEW_SCORER);
-        when(mNetworkScorerAppManager.setActiveScorer(NEW_SCORER.mPackageName)).thenReturn(true);
-        mNetworkScoreService.registerNetworkScoreCache(NetworkKey.TYPE_WIFI, mNetworkScoreCache,
-                CACHE_FILTER_NONE);
-
-        boolean success = mNetworkScoreService.setActiveScorer(NEW_SCORER.mPackageName);
-
-        assertTrue(success);
-        verify(mNetworkScoreCache).clearScores();
-        verify(mContext).bindServiceAsUser(MockUtils.checkIntent(new Intent().setComponent(
-                new ComponentName(NEW_SCORER.mPackageName, NEW_SCORER.mScoringServiceClassName))),
-                any(ServiceConnection.class),
-                eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE),
-                eq(UserHandle.SYSTEM));
-        verify(mContext, times(2)).sendBroadcastAsUser(
-                MockUtils.checkIntentAction(NetworkScoreManager.ACTION_SCORER_CHANGED),
-                eq(UserHandle.SYSTEM));
-    }
-
-    @Test
     public void testDisableScoring_notActiveScorer_noBroadcastNetworkPermission() {
         when(mNetworkScorerAppManager.isCallerActiveScorer(anyInt())).thenReturn(false);
         when(mContext.checkCallingOrSelfPermission(permission.BROADCAST_NETWORK_PRIVILEGED))
@@ -338,48 +386,6 @@
         } catch (SecurityException e) {
             // expected
         }
-
-    }
-
-    @Test
-    public void testDisableScoring_activeScorer() throws RemoteException {
-        when(mNetworkScorerAppManager.isCallerActiveScorer(anyInt())).thenReturn(true);
-        when(mNetworkScorerAppManager.getActiveScorer()).thenReturn(PREV_SCORER, null);
-        when(mNetworkScorerAppManager.setActiveScorer(null)).thenReturn(true);
-        mNetworkScoreService.registerNetworkScoreCache(NetworkKey.TYPE_WIFI, mNetworkScoreCache,
-                CACHE_FILTER_NONE);
-
-        mNetworkScoreService.disableScoring();
-
-        verify(mNetworkScoreCache).clearScores();
-        verify(mContext).sendBroadcastAsUser(
-                MockUtils.checkIntent(new Intent(NetworkScoreManager.ACTION_SCORER_CHANGED)
-                        .setPackage(PREV_SCORER.mPackageName)),
-                eq(UserHandle.SYSTEM));
-        verify(mContext, never()).bindServiceAsUser(any(Intent.class),
-                any(ServiceConnection.class), anyInt(), any(UserHandle.class));
-    }
-
-    @Test
-    public void testDisableScoring_notActiveScorer_hasBroadcastNetworkPermission()
-            throws RemoteException {
-        when(mNetworkScorerAppManager.isCallerActiveScorer(anyInt())).thenReturn(false);
-        when(mContext.checkCallingOrSelfPermission(permission.BROADCAST_NETWORK_PRIVILEGED))
-                .thenReturn(PackageManager.PERMISSION_GRANTED);
-        when(mNetworkScorerAppManager.getActiveScorer()).thenReturn(PREV_SCORER, null);
-        when(mNetworkScorerAppManager.setActiveScorer(null)).thenReturn(true);
-        mNetworkScoreService.registerNetworkScoreCache(NetworkKey.TYPE_WIFI, mNetworkScoreCache,
-                CACHE_FILTER_NONE);
-
-        mNetworkScoreService.disableScoring();
-
-        verify(mNetworkScoreCache).clearScores();
-        verify(mContext).sendBroadcastAsUser(
-                MockUtils.checkIntent(new Intent(NetworkScoreManager.ACTION_SCORER_CHANGED)
-                        .setPackage(PREV_SCORER.mPackageName)),
-                eq(UserHandle.SYSTEM));
-        verify(mContext, never()).bindServiceAsUser(any(Intent.class),
-                any(ServiceConnection.class), anyInt(), any(UserHandle.class));
     }
 
     @Test
@@ -434,4 +440,24 @@
 
         assertFalse(stringWriter.toString().isEmpty());
     }
+
+    // "injects" the mock INetworkRecommendationProvider into the NetworkScoreService.
+    private void injectProvider() {
+        final ComponentName componentName = new ComponentName(NEW_SCORER.packageName,
+                NEW_SCORER.recommendationServiceClassName);
+        when(mNetworkScorerAppManager.getActiveScorer()).thenReturn(NEW_SCORER);
+        when(mContext.bindServiceAsUser(isA(Intent.class), isA(ServiceConnection.class), anyInt(),
+                isA(UserHandle.class))).thenAnswer(new Answer<Boolean>() {
+            @Override
+            public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                IBinder mockBinder = mock(IBinder.class);
+                when(mockBinder.queryLocalInterface(anyString()))
+                        .thenReturn(mRecommendationProvider);
+                invocation.getArgumentAt(1, ServiceConnection.class)
+                        .onServiceConnected(componentName, mockBinder);
+                return true;
+            }
+        });
+        mNetworkScoreService.systemRunning();
+    }
 }