API for requesting network recommendations.

Defining a new system API that will allow the system to request
network recommendations from a NetworkScoreService implementation.

Test: Coming after the API is approved.
BUG: 32909424
Change-Id: I2d5c0a843b928b04e87c1862a78702a02fd54c31
diff --git a/Android.mk b/Android.mk
index 405f957..7099848 100644
--- a/Android.mk
+++ b/Android.mk
@@ -213,6 +213,7 @@
 	core/java/android/net/INetworkManagementEventObserver.aidl \
 	core/java/android/net/INetworkPolicyListener.aidl \
 	core/java/android/net/INetworkPolicyManager.aidl \
+	core/java/android/net/INetworkRecommendationProvider.aidl \
 	core/java/android/net/INetworkScoreCache.aidl \
 	core/java/android/net/INetworkScoreService.aidl \
 	core/java/android/net/INetworkStatsService.aidl \
diff --git a/api/system-current.txt b/api/system-current.txt
index f23c1fc..663a9c0 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -25957,6 +25957,14 @@
     field public final android.net.WifiKey wifiKey;
   }
 
+  public abstract class NetworkRecommendationProvider {
+    ctor public NetworkRecommendationProvider(android.os.Handler);
+    method public final android.os.IBinder getBinder();
+    method public abstract android.net.RecommendationResult onRequestRecommendation(android.net.RecommendationRequest);
+    field public static final java.lang.String EXTRA_RECOMMENDATION_RESULT = "android.net.extra.RECOMMENDATION_RESULT";
+    field public static final java.lang.String EXTRA_SEQUENCE = "android.net.extra.SEQUENCE";
+  }
+
   public class NetworkRequest implements android.os.Parcelable {
     method public int describeContents();
     method public void writeToParcel(android.os.Parcel, int);
@@ -25977,10 +25985,12 @@
     method public boolean clearScores() throws java.lang.SecurityException;
     method public void disableScoring() throws java.lang.SecurityException;
     method public java.lang.String getActiveScorerPackage();
+    method public android.net.RecommendationResult requestRecommendation(android.net.RecommendationRequest) throws java.lang.SecurityException;
     method public boolean setActiveScorer(java.lang.String) throws java.lang.SecurityException;
     method public boolean updateScores(android.net.ScoredNetwork[]) throws java.lang.SecurityException;
     field public static final java.lang.String ACTION_CHANGE_ACTIVE = "android.net.scoring.CHANGE_ACTIVE";
     field public static final java.lang.String ACTION_CUSTOM_ENABLE = "android.net.scoring.CUSTOM_ENABLE";
+    field public static final java.lang.String ACTION_RECOMMEND_NETWORKS = "android.net.action.RECOMMEND_NETWORKS";
     field public static final java.lang.String ACTION_SCORER_CHANGED = "android.net.scoring.SCORER_CHANGED";
     field public static final java.lang.String ACTION_SCORE_NETWORKS = "android.net.scoring.SCORE_NETWORKS";
     field public static final java.lang.String EXTRA_NETWORKS_TO_SCORE = "networksToScore";
@@ -26015,6 +26025,24 @@
     field public static final android.os.Parcelable.Creator<android.net.ProxyInfo> CREATOR;
   }
 
+  public final class RecommendationRequest implements android.os.Parcelable {
+    ctor protected RecommendationRequest(android.os.Parcel);
+    method public int describeContents();
+    method public android.net.wifi.WifiConfiguration getCurrentSelectedConfig();
+    method public android.net.NetworkCapabilities getRequiredCapabilities();
+    method public android.net.wifi.ScanResult[] getScanResults();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.net.RecommendationRequest> CREATOR;
+  }
+
+  public final class RecommendationResult implements android.os.Parcelable {
+    ctor public RecommendationResult(android.net.wifi.WifiConfiguration);
+    method public int describeContents();
+    method public android.net.wifi.WifiConfiguration getWifiConfiguration();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.net.RecommendationResult> CREATOR;
+  }
+
   public final class RouteInfo implements android.os.Parcelable {
     method public int describeContents();
     method public android.net.IpPrefix getDestination();
@@ -26068,9 +26096,12 @@
   public class ScoredNetwork implements android.os.Parcelable {
     ctor public ScoredNetwork(android.net.NetworkKey, android.net.RssiCurve);
     ctor public ScoredNetwork(android.net.NetworkKey, android.net.RssiCurve, boolean);
+    ctor public ScoredNetwork(android.net.NetworkKey, android.net.RssiCurve, boolean, android.os.Bundle);
     method public int describeContents();
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator<android.net.ScoredNetwork> CREATOR;
+    field public static final java.lang.String EXTRA_HAS_CAPTIVE_PORTAL = "android.net.extra.HAS_CAPTIVE_PORTAL";
+    field public final android.os.Bundle attributes;
     field public final boolean meteredHint;
     field public final android.net.NetworkKey networkKey;
     field public final android.net.RssiCurve rssiCurve;
diff --git a/core/java/android/net/INetworkRecommendationProvider.aidl b/core/java/android/net/INetworkRecommendationProvider.aidl
new file mode 100644
index 0000000..5e455d3
--- /dev/null
+++ b/core/java/android/net/INetworkRecommendationProvider.aidl
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2016, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.net.RecommendationRequest;
+import android.os.IRemoteCallback;
+
+/**
+ * The service responsible for answering network recommendation requests.
+ * @hide
+ */
+oneway interface INetworkRecommendationProvider {
+
+    /**
+     * Request a recommendation for the best network to connect to
+     * taking into account the inputs from the {@link RecommendationRequest}.
+     *
+     * @param request a {@link RecommendationRequest} instance containing the details of the request
+     * @param callback a {@link IRemoteCallback} instance to invoke when the recommendation
+     *                 is available
+     * @param sequence an internal number used for tracking the request
+     * @hide
+     */
+    void requestRecommendation(in RecommendationRequest request,
+                               in IRemoteCallback callback,
+                               int sequence);
+}
\ No newline at end of file
diff --git a/core/java/android/net/INetworkScoreService.aidl b/core/java/android/net/INetworkScoreService.aidl
index 43869264..59cbf6e 100644
--- a/core/java/android/net/INetworkScoreService.aidl
+++ b/core/java/android/net/INetworkScoreService.aidl
@@ -17,6 +17,8 @@
 package android.net;
 
 import android.net.INetworkScoreCache;
+import android.net.RecommendationRequest;
+import android.net.RecommendationResult;
 import android.net.ScoredNetwork;
 
 /**
@@ -64,4 +66,14 @@
      */
     void registerNetworkScoreCache(int networkType, INetworkScoreCache scoreCache);
 
+    /**
+     * Request a recommendation for the best network to connect to
+     * taking into account the inputs from the {@link RecommendationRequest}.
+     *
+     * @param request a {@link RecommendationRequest} instance containing the details of the request
+     * @return a {@link RecommendationResult} containing the recommended network to connect to
+     * @throws SecurityException if the caller is not the system
+     */
+    RecommendationResult requestRecommendation(in RecommendationRequest request);
+
 }
diff --git a/core/java/android/net/NetworkRecommendationProvider.java b/core/java/android/net/NetworkRecommendationProvider.java
new file mode 100644
index 0000000..cd2ede8
--- /dev/null
+++ b/core/java/android/net/NetworkRecommendationProvider.java
@@ -0,0 +1,114 @@
+package android.net;
+
+import android.annotation.SystemApi;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IRemoteCallback;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * The base class for implementing a network recommendation provider.
+ * @hide
+ */
+@SystemApi
+public abstract class NetworkRecommendationProvider {
+    private static final String TAG = "NetworkRecProvider";
+    /** The key into the callback Bundle where the RecommendationResult will be found. */
+    public static final String EXTRA_RECOMMENDATION_RESULT =
+            "android.net.extra.RECOMMENDATION_RESULT";
+    /** The key into the callback Bundle where the sequence will be found. */
+    public static final String EXTRA_SEQUENCE = "android.net.extra.SEQUENCE";
+    private static final String EXTRA_RECOMMENDATION_REQUEST =
+            "android.net.extra.RECOMMENDATION_REQUEST";
+    private final IBinder mService;
+
+    /**
+     * Constructs a new instance.
+     * @param handler indicates which thread to use when handling requests. Cannot be {@code null}.
+     */
+    public NetworkRecommendationProvider(Handler handler) {
+        if (handler == null) {
+            throw new IllegalArgumentException("The provided handler cannot be null.");
+        }
+        mService = new ServiceWrapper(new ServiceHandler(handler.getLooper()));
+    }
+
+    /**
+     * Invoked when a recommendation has been requested.
+     *
+     * @param request a {@link RecommendationRequest} instance containing additional
+     *                request details
+     * @return a {@link RecommendationResult} instance containing the recommended
+     *         network to connect to
+     */
+    public abstract RecommendationResult onRequestRecommendation(RecommendationRequest request);
+
+
+    /**
+     * Services that can handle {@link NetworkScoreManager#ACTION_RECOMMEND_NETWORKS} should
+     * return this Binder from their <code>onBind()</code> method.
+     */
+    public final IBinder getBinder() {
+        return mService;
+    }
+
+    private final class ServiceHandler extends Handler {
+        static final int MSG_GET_RECOMMENDATION = 1;
+
+        ServiceHandler(Looper looper) {
+            super(looper, null /*callback*/, true /*async*/);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            final int what = msg.what;
+            switch (what) {
+                case MSG_GET_RECOMMENDATION:
+                    final IRemoteCallback callback = (IRemoteCallback) msg.obj;
+                    final int seq = msg.arg1;
+                    final RecommendationRequest request =
+                            msg.getData().getParcelable(EXTRA_RECOMMENDATION_REQUEST);
+                    final RecommendationResult result = onRequestRecommendation(request);
+                    final Bundle data = new Bundle();
+                    data.putInt(EXTRA_SEQUENCE, seq);
+                    data.putParcelable(EXTRA_RECOMMENDATION_RESULT, result);
+                    try {
+                        callback.sendResult(data);
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "Callback failed for seq: " + seq, e);
+                    }
+
+                    break;
+
+                default:
+                    throw new IllegalArgumentException("Unknown message: " + what);
+            }
+        }
+    }
+
+    /**
+     * A wrapper around INetworkRecommendationProvider that sends calls to the internal Handler.
+     */
+    private static final class ServiceWrapper extends INetworkRecommendationProvider.Stub {
+        private final Handler mHandler;
+
+        ServiceWrapper(Handler handler) {
+            mHandler = handler;
+        }
+
+        @Override
+        public void requestRecommendation(RecommendationRequest request, IRemoteCallback callback,
+                int sequence) throws RemoteException {
+            final Message msg = mHandler.obtainMessage(
+                    ServiceHandler.MSG_GET_RECOMMENDATION, sequence, 0 /*arg2*/, callback);
+            final Bundle data = new Bundle();
+            data.putParcelable(EXTRA_RECOMMENDATION_REQUEST, request);
+            msg.setData(data);
+            msg.sendToTarget();
+        }
+    }
+}
diff --git a/core/java/android/net/NetworkScoreManager.java b/core/java/android/net/NetworkScoreManager.java
index a0f74ec..a2d2b58 100644
--- a/core/java/android/net/NetworkScoreManager.java
+++ b/core/java/android/net/NetworkScoreManager.java
@@ -117,6 +117,14 @@
     public static final String ACTION_SCORER_CHANGED = "android.net.scoring.SCORER_CHANGED";
 
     /**
+     * Service action: Used to discover and bind to a network recommendation provider.
+     * Implementations should return {@link NetworkRecommendationProvider#getBinder()} from
+     * their <code>onBind()</code> method.
+     */
+    @SdkConstant(SdkConstantType.SERVICE_ACTION)
+    public static final String ACTION_RECOMMEND_NETWORKS = "android.net.action.RECOMMEND_NETWORKS";
+
+    /**
      * Extra used with {@link #ACTION_SCORER_CHANGED} to specify the newly selected scorer's package
      * name. Will be null if scoring was disabled. Can be obtained with
      * {@link android.content.Intent#getStringExtra(String)}.
@@ -269,4 +277,22 @@
             throw e.rethrowFromSystemServer();
         }
     }
+
+    /**
+     * Request a recommendation for which network to connect to.
+     *
+     * @param request a {@link RecommendationRequest} instance containing additional
+     *                request details
+     * @return a {@link RecommendationResult} instance containing the recommended network
+     *         to connect to
+     * @throws SecurityException
+     */
+    public RecommendationResult requestRecommendation(RecommendationRequest request)
+            throws SecurityException {
+        try {
+            return mService.requestRecommendation(request);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/core/java/android/net/RecommendationRequest.aidl b/core/java/android/net/RecommendationRequest.aidl
new file mode 100644
index 0000000..76497b8
--- /dev/null
+++ b/core/java/android/net/RecommendationRequest.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2016, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+parcelable RecommendationRequest;
diff --git a/core/java/android/net/RecommendationRequest.java b/core/java/android/net/RecommendationRequest.java
new file mode 100644
index 0000000..05ca1aa
--- /dev/null
+++ b/core/java/android/net/RecommendationRequest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.net;
+
+
+import android.annotation.SystemApi;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * A request for a network recommendation.
+ *
+ * @see {@link NetworkScoreManager#requestRecommendation(RecommendationRequest)}.
+ * @hide
+ */
+@SystemApi
+public final class RecommendationRequest implements Parcelable {
+    private final ScanResult[] mScanResults;
+    private final WifiConfiguration mCurrentSelectedConfig;
+    private final NetworkCapabilities mRequiredCapabilities;
+
+    /**
+     * Builder class for constructing {@link RecommendationRequest} instances.
+     * @hide
+     */
+    public static final class Builder {
+        private ScanResult[] mScanResults;
+        private WifiConfiguration mCurrentConfig;
+        private NetworkCapabilities mNetworkCapabilities;
+
+        public Builder setScanResults(ScanResult[] scanResults) {
+            mScanResults = scanResults;
+            return this;
+        }
+
+        public Builder setCurrentRecommendedWifiConfig(WifiConfiguration config) {
+            this.mCurrentConfig = config;
+            return this;
+        }
+
+        public Builder setNetworkCapabilities(NetworkCapabilities capabilities) {
+            mNetworkCapabilities = capabilities;
+            return this;
+        }
+
+        public RecommendationRequest build() {
+            return new RecommendationRequest(mScanResults, mCurrentConfig, mNetworkCapabilities);
+        }
+    }
+
+    /**
+     * @return the array of {@link ScanResult}s the recommendation must be constrained to i.e. if a
+     *         non-null wifi config recommendation is returned then it must be able to connect to
+     *         one of the networks in the results list.
+     *
+     *         If the array is {@code null} or empty then there is no constraint.
+     */
+    public ScanResult[] getScanResults() {
+        return mScanResults;
+    }
+
+    /**
+     * @return The best recommendation at the time this {@code RecommendationRequest} instance
+     *         was created. This may be null which indicates that no recommendation is available.
+     */
+    public WifiConfiguration getCurrentSelectedConfig() {
+        return mCurrentSelectedConfig;
+    }
+
+    /**
+     *
+     * @return The set of {@link NetworkCapabilities} the recommendation must be constrained to.
+     *         This may be {@code null} which indicates that there are no constraints on the
+     *         capabilities of the recommended network.
+     */
+    public NetworkCapabilities getRequiredCapabilities() {
+        return mRequiredCapabilities;
+    }
+
+    @VisibleForTesting
+    RecommendationRequest(ScanResult[] scanResults,
+            WifiConfiguration currentSelectedConfig,
+            NetworkCapabilities requiredCapabilities) {
+        mScanResults = scanResults;
+        mCurrentSelectedConfig = currentSelectedConfig;
+        mRequiredCapabilities = requiredCapabilities;
+    }
+
+    protected RecommendationRequest(Parcel in) {
+        mScanResults = (ScanResult[]) in.readParcelableArray(ScanResult.class.getClassLoader());
+        mCurrentSelectedConfig = in.readParcelable(WifiConfiguration.class.getClassLoader());
+        mRequiredCapabilities = in.readParcelable(NetworkCapabilities.class.getClassLoader());
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelableArray(mScanResults, flags);
+        dest.writeParcelable(mCurrentSelectedConfig, flags);
+        dest.writeParcelable(mRequiredCapabilities, flags);
+    }
+
+    public static final Creator<RecommendationRequest> CREATOR =
+            new Creator<RecommendationRequest>() {
+                @Override
+                public RecommendationRequest createFromParcel(Parcel in) {
+                    return new RecommendationRequest(in);
+                }
+
+                @Override
+                public RecommendationRequest[] newArray(int size) {
+                    return new RecommendationRequest[size];
+                }
+            };
+}
diff --git a/core/java/android/net/RecommendationResult.aidl b/core/java/android/net/RecommendationResult.aidl
new file mode 100644
index 0000000..f36995b
--- /dev/null
+++ b/core/java/android/net/RecommendationResult.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2016, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+parcelable RecommendationResult;
diff --git a/core/java/android/net/RecommendationResult.java b/core/java/android/net/RecommendationResult.java
new file mode 100644
index 0000000..a330d84
--- /dev/null
+++ b/core/java/android/net/RecommendationResult.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.net;
+
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.net.wifi.WifiConfiguration;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * The result of a network recommendation.
+ *
+ * @see {@link NetworkScoreManager#requestRecommendation(RecommendationRequest)}.
+ * @hide
+ */
+@SystemApi
+public final class RecommendationResult implements Parcelable {
+    private final WifiConfiguration mWifiConfiguration;
+
+    public RecommendationResult(@Nullable WifiConfiguration wifiConfiguration) {
+        mWifiConfiguration = wifiConfiguration;
+    }
+
+    private RecommendationResult(Parcel in) {
+        mWifiConfiguration = in.readParcelable(WifiConfiguration.class.getClassLoader());
+    }
+
+    /**
+     * @return The recommended {@link WifiConfiguration} to connect to. A {@code null} value
+     *         indicates that no WiFi connection should be attempted at this time.
+     */
+    public WifiConfiguration getWifiConfiguration() {
+        return mWifiConfiguration;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeParcelable(mWifiConfiguration, flags);
+    }
+
+    public static final Creator<RecommendationResult> CREATOR =
+            new Creator<RecommendationResult>() {
+                @Override
+                public RecommendationResult createFromParcel(Parcel in) {
+                    return new RecommendationResult(in);
+                }
+
+                @Override
+                public RecommendationResult[] newArray(int size) {
+                    return new RecommendationResult[size];
+                }
+            };
+}
diff --git a/core/java/android/net/ScoredNetwork.java b/core/java/android/net/ScoredNetwork.java
index 8582150..0f3f957 100644
--- a/core/java/android/net/ScoredNetwork.java
+++ b/core/java/android/net/ScoredNetwork.java
@@ -17,6 +17,7 @@
 package android.net;
 
 import android.annotation.SystemApi;
+import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -29,6 +30,20 @@
  */
 @SystemApi
 public class ScoredNetwork implements Parcelable {
+    /**
+     * Extra used with {@link #attributes} to specify whether the
+     * network is believed to have a captive portal.
+     * <p>
+     * This data may be used, for example, to display a visual indicator
+     * in a network selection list.
+     * <p>
+     * Note that the this extra conveys the possible presence of a
+     * captive portal, not its state or the user's ability to open
+     * the portal.
+     * <p>
+     * If no value is associated with this key then it's unknown.
+     */
+    public static final String EXTRA_HAS_CAPTIVE_PORTAL = "android.net.extra.HAS_CAPTIVE_PORTAL";
 
     /** A {@link NetworkKey} uniquely identifying this network. */
     public final NetworkKey networkKey;
@@ -53,6 +68,14 @@
     public final boolean meteredHint;
 
     /**
+     * An additional collection of optional attributes set by
+     * the Network Recommendation Provider.
+     *
+     * @see #EXTRA_HAS_CAPTIVE_PORTAL
+     */
+    public final Bundle attributes;
+
+    /**
      * Construct a new {@link ScoredNetwork}.
      *
      * @param networkKey the {@link NetworkKey} uniquely identifying this network.
@@ -81,9 +104,29 @@
      *     metered.
      */
     public ScoredNetwork(NetworkKey networkKey, RssiCurve rssiCurve, boolean meteredHint) {
+        this(networkKey, rssiCurve, false /* meteredHint */, null /* attributes */);
+    }
+
+    /**
+     * Construct a new {@link ScoredNetwork}.
+     *
+     * @param networkKey the {@link NetworkKey} uniquely identifying this network
+     * @param rssiCurve the {@link RssiCurve} representing the scores for this network based on the
+     *     RSSI. This field is optional, and may be skipped to represent a network which the scorer
+     *     has opted not to score at this time. Passing a null value here is strongly preferred to
+     *     not returning any {@link ScoredNetwork} for a given {@link NetworkKey} because it
+     *     indicates to the system not to request scores for this network in the future, although
+     *     the scorer may choose to issue an out-of-band update at any time.
+     * @param meteredHint a boolean value indicating whether or not the network is believed to be
+     *                    metered
+     * @param attributes optional provider specific attributes
+     */
+    public ScoredNetwork(NetworkKey networkKey, RssiCurve rssiCurve, boolean meteredHint,
+            Bundle attributes) {
         this.networkKey = networkKey;
         this.rssiCurve = rssiCurve;
         this.meteredHint = meteredHint;
+        this.attributes = attributes;
     }
 
     private ScoredNetwork(Parcel in) {
@@ -94,6 +137,7 @@
             rssiCurve = null;
         }
         meteredHint = in.readByte() != 0;
+        attributes = in.readBundle();
     }
 
     @Override
@@ -111,6 +155,8 @@
             out.writeByte((byte) 0);
         }
         out.writeByte((byte) (meteredHint ? 1 : 0));
+        out.writeBundle(attributes);
+
     }
 
     @Override
@@ -121,19 +167,24 @@
         ScoredNetwork that = (ScoredNetwork) o;
 
         return Objects.equals(networkKey, that.networkKey)
-            && Objects.equals(rssiCurve, that.rssiCurve)
-            && Objects.equals(meteredHint, that.meteredHint);
+                && Objects.equals(rssiCurve, that.rssiCurve)
+                && Objects.equals(meteredHint, that.meteredHint)
+                && Objects.equals(attributes, that.attributes);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(networkKey, rssiCurve, meteredHint);
+        return Objects.hash(networkKey, rssiCurve, meteredHint, attributes);
     }
 
     @Override
     public String toString() {
-        return "ScoredNetwork[key=" + networkKey + ",score=" + rssiCurve
-            + ",meteredHint=" + meteredHint + "]";
+        return "ScoredNetwork{" +
+                "networkKey=" + networkKey +
+                ", rssiCurve=" + rssiCurve +
+                ", meteredHint=" + meteredHint +
+                ", attributes=" + attributes +
+                '}';
     }
 
     public static final Parcelable.Creator<ScoredNetwork> CREATOR =
diff --git a/services/core/java/com/android/server/NetworkScoreService.java b/services/core/java/com/android/server/NetworkScoreService.java
index 72fa1e3..4c9ea58 100644
--- a/services/core/java/com/android/server/NetworkScoreService.java
+++ b/services/core/java/com/android/server/NetworkScoreService.java
@@ -30,7 +30,10 @@
 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.wifi.WifiConfiguration;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -417,6 +420,16 @@
     }
 
     @Override
+    public RecommendationResult requestRecommendation(RecommendationRequest request) {
+        // TODO(jjoslin): 11/25/16 - Update with real impl.
+        WifiConfiguration selectedConfig = null;
+        if (request != null) {
+            selectedConfig = request.getCurrentSelectedConfig();
+        }
+        return new RecommendationResult(selectedConfig);
+    }
+
+    @Override
     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
         mContext.enforceCallingOrSelfPermission(permission.DUMP, TAG);
         NetworkScorerAppData currentScorer = mNetworkScorerAppManager.getActiveScorer();